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

# Centrifuge Integration

> Use a Para-powered wallet to deposit into and redeem from Centrifuge vaults on Base and X Layer.

This walkthrough explains how to integrate Centrifuge vaults on Base and X Layer with a Para-powered EVM wallet. It is a step-by-step guide for depositing into and redeeming from Centrifuge vaults in a TypeScript web application.

The vault sits in front of the Centrifuge protocol so integrators don't have to deal with its complexity: you deposit USDC into the vault and receive the deRWA share token, which is transferable between non-frozen accounts.

## Overview

The vaults in this guide provide a small contract interface for investing in tokenized real-world asset products. Users deposit an accepted asset, such as USDC, and receive product shares. Depending on the vault configuration, a deposit either completes immediately or enters a settlement queue.

Redemptions are always asynchronous. The user submits a redemption request, waits for settlement, and then claims the resulting asset.

The integration must therefore distinguish between:

* **Synchronous deposit:** one transaction deposits the asset and returns deRWA tokens.
* **Asynchronous deposit:** one transaction creates the request and a later transaction claims deRWA tokens.
* **Asynchronous redemption:** one transaction creates the request and a later transaction claims the asset.

## Reference Deployments

The examples use five live `PassthroughVault` deployments across two independent chains. Each chain has its own RPC endpoint, gas token, and USDC contract.

### Base (Chain ID 8453, Gas in ETH)

| Product                             | Deposit mode                                 | Vault address                                | Accepted asset    | Asset address                                | deRWA token                                  |
| ----------------------------------- | -------------------------------------------- | -------------------------------------------- | ----------------- | -------------------------------------------- | -------------------------------------------- |
| JSPX (formerly deSPXA, 18 decimals) | Fully asynchronous                           | `0x86faaBE66124Fe9027BEC5d920AdF7aF0590cECC` | USDC (6 decimals) | `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` | `0x9c5C365e764829876243d0b289733B9D2b729685` |
| deJAAA (18 decimals)                | Synchronous deposit, asynchronous redemption | `0x3f24925123deAcec58CD122BFD329907B8038712` | USDC (6 decimals) | `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` | `0xAAA0008C8CF3A7Dca931adaF04336A5D808C82Cc` |

### X Layer (Chain ID 196, Gas in OKB)

| Product               | Deposit mode                                 | Vault address                                | Accepted asset    | Asset address                                | deRWA token                                  |
| --------------------- | -------------------------------------------- | -------------------------------------------- | ----------------- | -------------------------------------------- | -------------------------------------------- |
| deJAAA (18 decimals)  | Synchronous deposit, asynchronous redemption | `0xddD4E1F19DA8CAF2702840784D8D930d661d51c4` | USDC (6 decimals) | `0xB6CEceAB302E2E4948951eE7843FC24E92933061` | `0x5F8a1C74C112865BD05dbe4752C7608332719062` |
| deHYB (18 decimals)   | Synchronous deposit, asynchronous redemption | `0xD55716089C722e8086A53AF180D528207AC0E753` | USDC (6 decimals) | `0xB6CEceAB302E2E4948951eE7843FC24E92933061` | `0xc5A9F6EdB48160eD9d9FB156A23c39d7140457eE` |
| deJTRSY (18 decimals) | Synchronous deposit, asynchronous redemption | `0x61506f58f12ff371b0ea88764cc09fe7d86af1d6` | USDC (6 decimals) | `0xB6CEceAB302E2E4948951eE7843FC24E92933061` | `0x8DE0F3295B9e42b29E7617BAdA7C603277420451` |

<Note>
  The share token at `0x9c5C...` was renamed onchain from deSPXA to JSPX on August 17, 2026. The address and 18-decimal precision did not change.
</Note>

For protocol-level contracts and newly supported networks, see the [Centrifuge deployments reference](https://docs.centrifuge.io/developer/protocol/deployments/). Confirm product-specific passthrough vault addresses with Centrifuge before adding another deployment.

## How It Works

Across the listed deployments, deposits are **synchronous** for deJAAA, deHYB, and deJTRSY, and **asynchronous** for JSPX. Redemptions are **asynchronous** (ERC-7540 style): you submit a request, the issuer fulfills it during their settlement cycle (this can take hours or days), and then you claim the result.

Deposit mode is configured per passthrough vault, not per chain or product. Read `asyncDeposit()` from the vault instead of assuming a mode from its name.

Three things to know before writing any code:

1. **There is no `claim()` function.** Claiming a fulfilled deposit is done by calling `deposit(...)`, and claiming a fulfilled redemption by calling `redeem(...)`.
2. **Requests are self-directed.** The `controller` and `owner` parameters in the request functions must be your own address.
3. **`mint()` and `withdraw()` don't exist.** Only `deposit()` and `redeem()` are supported, so don't point generic ERC-4626/7540 tooling at this vault.

<Note>
  These deployments allow anyone to claim a controller's entire settled balance on their behalf, but the receiver must be that same controller. Partial third-party claims are not supported.
</Note>

## What You Need

* Node.js 18 or later for project tooling.
* A package manager such as npm, pnpm, or yarn.
* A Para API key, a completed [Para authentication flow](/v3/react/guides/custom-ui-web-sdk), and an EVM wallet for the authenticated user.
* The Para Web SDK, Para's viem integration, and viem.
* A browser-exposed mainnet RPC URL for Base or X Layer.
* The selected chain's gas token: ETH on Base or OKB on X Layer.
* USDC on the selected chain for deposits.
* The authoritative vault ABI and a standard ERC-20 ABI.
* A user interface that represents pending and claimable states separately.
* Para handles signing without exposing or storing the user's private key in your application.

### Install Dependencies

```bash theme={null}
npm install @getpara/web-sdk @getpara/viem-v2-integration viem
```

***

## Step-by-Step Integration (TypeScript + Para + viem)

All the code in this guide lives in a single file and uses Para's Web SDK, Para's viem integration, and [viem](https://viem.sh).

### Setup

Start with the Para and viem imports:

```ts theme={null}
import Para from '@getpara/web-sdk'
import {
  createParaViemAccount,
  createParaViemClient,
} from '@getpara/viem-v2-integration'
import {
  createPublicClient,
  http,
  parseAbi,
  parseUnits,
  maxUint256,
} from 'viem'
import { base, xLayer } from 'viem/chains'
```

Define the supported deployments in one place. This walkthrough selects Base by default; change `selectedNetwork` to `deployments.xLayer` to use an X Layer sync-deposit or redemption flow.

```ts theme={null}
const deployments = {
  base: {
    chain: base,
    rpcUrl: 'YOUR_BASE_RPC_URL',
    usdc: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
    products: {
      jspx: {
        vault: '0x86faaBE66124Fe9027BEC5d920AdF7aF0590cECC',
        share: '0x9c5C365e764829876243d0b289733B9D2b729685',
      },
      deJAAA: {
        vault: '0x3f24925123deAcec58CD122BFD329907B8038712',
        share: '0xAAA0008C8CF3A7Dca931adaF04336A5D808C82Cc',
      },
    },
  },
  xLayer: {
    chain: xLayer,
    rpcUrl: 'YOUR_X_LAYER_RPC_URL',
    usdc: '0xB6CEceAB302E2E4948951eE7843FC24E92933061',
    products: {
      deJAAA: {
        vault: '0xddD4E1F19DA8CAF2702840784D8D930d661d51c4',
        share: '0x5F8a1C74C112865BD05dbe4752C7608332719062',
      },
      deHYB: {
        vault: '0xD55716089C722e8086A53AF180D528207AC0E753',
        share: '0xc5A9F6EdB48160eD9d9FB156A23c39d7140457eE',
      },
      deJTRSY: {
        vault: '0x61506f58f12ff371b0ea88764cc09fe7d86af1d6',
        share: '0x8DE0F3295B9e42b29E7617BAdA7C603277420451',
      },
    },
  },
} as const

const selectedNetwork = deployments.base
const syncProduct = selectedNetwork.products.deJAAA
const redemptionProduct = selectedNetwork.products.deJAAA
const asyncProduct = deployments.base.products.jspx
```

We only need `approve` and `balanceOf` from the ERC-20s:

```ts theme={null}
const erc20Abi = parseAbi([
  'function approve(address spender, uint256 amount) returns (bool)',
  'function balanceOf(address owner) view returns (uint256)',
])
```

And from the vault, the request/claim functions plus the two views that track a request's lifecycle in each direction:

```ts theme={null}
const vaultAbi = parseAbi([
  'function asyncDeposit() view returns (bool)',
  // deposit flow
  'function requestDeposit(uint256 assets, address controller, address owner) returns (uint256)',
  'function deposit(uint256 assets, address receiver) returns (uint256 shares)',
  'function deposit(uint256 assets, address receiver, address controller) returns (uint256 shares)',
  'function pendingDepositRequest(uint256, address controller) view returns (uint256)',
  'function claimableDepositRequest(uint256, address controller) view returns (uint256)',
  // redeem flow
  'function requestRedeem(uint256 shares, address controller, address owner) returns (uint256)',
  'function redeem(uint256 shares, address receiver, address controller) returns (uint256 assets)',
  'function pendingRedeemRequest(uint256, address controller) view returns (uint256)',
  'function claimableRedeemRequest(uint256, address controller) view returns (uint256)',
])
```

Complete Para authentication before creating the account and clients. `createParaViemAccount` selects the first available EVM wallet unless you pass a specific `address` or `walletId`.

```ts theme={null}
const para = new Para('YOUR_PARA_API_KEY')

if (!(await para.isFullyLoggedIn())) {
  throw new Error('Authenticate with Para before signing transactions')
}

const account = createParaViemAccount({ para })

const publicClient = createPublicClient({
  chain: selectedNetwork.chain,
  transport: http(selectedNetwork.rpcUrl),
})

const walletClient = createParaViemClient({
  para,
  walletClientConfig: {
    account,
    chain: selectedNetwork.chain,
    transport: http(selectedNetwork.rpcUrl),
  },
})
```

Finally, a small helper so every write waits for inclusion and fails loudly on revert:

```ts theme={null}
async function sendAndWait(txPromise: Promise<`0x${string}`>) {
  const hash = await txPromise
  const receipt = await publicClient.waitForTransactionReceipt({ hash })
  if (receipt.status !== 'success') throw new Error(`Tx reverted: ${hash}`)
  return receipt
}
```

That's all the setup. Everything below uses these constants, clients, and the helper directly.

### Case 1: Sync Deposit

In sync mode there is no request and no claim: approve USDC, call `deposit`, and the shares arrive in the same transaction.

```ts theme={null}
async function syncDeposit(usdcAmount: string) {
  const assets = parseUnits(usdcAmount, 6)

  await sendAndWait(walletClient.writeContract({
    address: selectedNetwork.usdc, abi: erc20Abi, functionName: 'approve',
    args: [syncProduct.vault, assets],
  }))

  await sendAndWait(walletClient.writeContract({
    address: syncProduct.vault, abi: vaultAbi, functionName: 'deposit',
    args: [assets, account.address],
  }))
}
```

### Case 2: Async Deposit

JSPX on Base is the only asynchronous-deposit product in this guide. This case requires `selectedNetwork = deployments.base` and uses `asyncProduct` from that deployment.

#### Step 1: Approve USDC

The vault pulls your USDC when you submit the request, so it needs an allowance first. USDC uses **6 decimals**:

```ts theme={null}
const assets = parseUnits('1000', 6) // 1,000 USDC

await sendAndWait(walletClient.writeContract({
  address: selectedNetwork.usdc, abi: erc20Abi, functionName: 'approve',
  args: [asyncProduct.vault, assets],
}))
```

#### Step 2: Submit the Request

`requestDeposit` takes the amount plus a `controller` and an `owner`. Both must be **your own address** (the caller). Your USDC is transferred immediately and the request enters the queue:

```ts theme={null}
await sendAndWait(walletClient.writeContract({
  address: asyncProduct.vault, abi: vaultAbi, functionName: 'requestDeposit',
  args: [assets, account.address, account.address],
}))
```

#### Step 3: Wait for Fulfillment

The issuer settles requests in epochs, so fulfillment is not instant. Two views, both denominated in USDC, tell you where your request stands: `pending` is what's still queued and `claimable` is what's already settled and ready.

```ts theme={null}
async function getDepositStatus(controller = account.address) {
  const [pending, claimable] = await Promise.all([
    publicClient.readContract({
      address: asyncProduct.vault, abi: vaultAbi,
      functionName: 'pendingDepositRequest', args: [0n, controller],
    }),
    publicClient.readContract({
      address: asyncProduct.vault, abi: vaultAbi,
      functionName: 'claimableDepositRequest', args: [0n, controller],
    }),
  ])
  return { pending, claimable }
}
```

Fulfillment can also be **partial**: requests settle in order across all investors, so `claimable` may cover only part of your request for a while. You can claim partial amounts as they become available.

<Note>
  In a real service, poll these views from a background job or cron. Fulfillment can take hours or days depending on the product's settlement cycle.
</Note>

<Warning>
  Do not use `pending === 0` as the only terminal signal. Rounding dust can leave a small pending amount that is not claimable, and these passthrough vaults do not expose request cancellation.
</Warning>

#### Step 4: Claim Your Shares

Once `claimable > 0`, claim by calling `deposit`. Passing `maxUint256` as the amount claims everything that's ready, so you don't have to compute it first:

```ts theme={null}
async function claimDeposit() {
  const { claimable } = await getDepositStatus()
  if (claimable === 0n) throw new Error('Nothing claimable yet')

  await sendAndWait(walletClient.writeContract({
    address: asyncProduct.vault, abi: vaultAbi, functionName: 'deposit',
    args: [maxUint256, account.address],
  }))
}
```

The JSPX shares (18 decimals) are now in your wallet:

```ts theme={null}
const jspxBalance = await publicClient.readContract({
  address: asyncProduct.share, abi: erc20Abi, functionName: 'balanceOf',
  args: [account.address],
})
```

One nice property: if you submit a **new** `requestDeposit` while you still have a claimable balance from a previous one, the vault auto-claims the old one to you first. You'll never strand settled funds by re-requesting.

### Case 3: Async Redemption

The redemption flow mirrors the asynchronous deposit flow: request, wait, and claim, but moving shares in and USDC out. The selected `redemptionProduct` can be any of the five products listed above as long as it belongs to `selectedNetwork`.

#### Step 1: Approve Shares

`requestRedeem` pulls the selected product's shares via `transferFrom`, so approve the vault first. All share tokens listed in this guide use **18 decimals**:

```ts theme={null}
const sharesToRedeem = parseUnits('500', 18)

await sendAndWait(walletClient.writeContract({
  address: redemptionProduct.share, abi: erc20Abi, functionName: 'approve',
  args: [redemptionProduct.vault, sharesToRedeem],
}))
```

#### Step 2: Submit the Request

Same rule as deposits: `controller` and `owner` must be your own address.

```ts theme={null}
await sendAndWait(walletClient.writeContract({
  address: redemptionProduct.vault, abi: vaultAbi, functionName: 'requestRedeem',
  args: [sharesToRedeem, account.address, account.address],
}))
```

#### Step 3: Wait for Fulfillment

Same pattern as deposits, with the redeem-side views. Note these are denominated in **shares**, not USDC:

```ts theme={null}
async function getRedeemStatus(controller = account.address) {
  const [pending, claimable] = await Promise.all([
    publicClient.readContract({
      address: redemptionProduct.vault, abi: vaultAbi,
      functionName: 'pendingRedeemRequest', args: [0n, controller],
    }),
    publicClient.readContract({
      address: redemptionProduct.vault, abi: vaultAbi,
      functionName: 'claimableRedeemRequest', args: [0n, controller],
    }),
  ])
  return { pending, claimable }
}
```

#### Step 4: Claim Your USDC

Claim by calling `redeem`. Again, `maxUint256` claims everything that's settled:

```ts theme={null}
async function claimRedeem() {
  const { claimable } = await getRedeemStatus()
  if (claimable === 0n) throw new Error('Nothing claimable yet')

  await sendAndWait(walletClient.writeContract({
    address: redemptionProduct.vault, abi: vaultAbi, functionName: 'redeem',
    args: [maxUint256, account.address, account.address],
  }))
}
```

<Note>
  The example selects deJAAA. On Base, set `redemptionProduct` to `selectedNetwork.products.jspx` to redeem JSPX. On X Layer, select `deJAAA`, `deHYB`, or `deJTRSY` from `selectedNetwork.products`.
</Note>

## Conclusions

These passthrough vaults use the same transaction interface across Base and X Layer. Synchronous deposits complete atomically, while JSPX deposits and every listed product's redemptions use a request-and-claim lifecycle. Selecting another listed deployment changes the chain, RPC URL, USDC address, vault address, and share-token address without changing Para's authentication or signing flow.
