# Issue an MPT for Confidential Transfers

This tutorial shows you how to issue a [Multi-Purpose Token (MPT)](/es-es/docs/concepts/tokens/fungible-tokens/multi-purpose-tokens) that supports [Confidential Transfers](/es-es/docs/concepts/tokens/fungible-tokens/confidential-transfers), so that account balances and transfer amounts stay encrypted on-ledger.

_Requires the [ConfidentialTransfer amendment](/resources/known-amendments#confidentialtransfer). (Open for Voting: 5.71%)_

## Goals

By the end of this tutorial, you will be able to:

- Generate the [EC-ElGamal](https://en.wikipedia.org/wiki/ElGamal_encryption) keypairs that confidential transfers require.
- Issue an MPT configured for confidential transfers.
- Merge an inbox balance into a spending balance and decrypt it to verify the result.


## Prerequisites

To complete this tutorial, you should:

- Have a basic understanding of the XRP Ledger.
- Understand the [Confidential Transfers](/es-es/docs/concepts/tokens/fungible-tokens/confidential-transfers) concept, in particular the [issuer second account model](/es-es/docs/concepts/tokens/fungible-tokens/confidential-transfers#issuer-second-account-model) and the [split-balance model](/es-es/docs/concepts/tokens/fungible-tokens/confidential-transfers#split-balance-model).
- Have an XRP Ledger client library set up in your development environment. This page provides examples for the following:
  - **JavaScript** with the [xrpl.js library](https://github.com/XRPLF/xrpl.js). See [Get Started Using JavaScript](/docs/tutorials/get-started/get-started-javascript) for setup steps.
  - **Python** with the [xrpl-py library](https://github.com/XRPLF/xrpl-py). See [Get Started Using Python](/docs/tutorials/get-started/get-started-python) for setup steps.


## Source Code

You can find the complete source code for this tutorial's examples in the code samples section of this website's repository.

## Steps

### 1. Install dependencies

JavaScript
From the code sample folder, use `npm` to install dependencies.

```bash
npm install
```

Python
From the code sample folder, set up a virtual environment and use `pip` to install dependencies.

```bash
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
```

### 2. Set up the client and accounts

To get started, import the necessary libraries and instantiate a client to connect to the XRP Ledger. This example imports:

JavaScript
- `fs`: Used to write the generated accounts and keys to a local file.
- `xrpl`: Used for XRPL client connection, transaction submission, and wallet handling.


```js
import fs from 'fs'
import {
  Client,
  MPTokenIssuanceCreateFlags,
  deriveConfidentialKeypair,
  encodeMPTokenMetadata,
  fetchMPToken,
  fetchMPTokenIssuance,
  loadMptCrypto,
  prepareConfidentialConvert,
  prepareConfidentialMergeInbox
} from 'xrpl'

// Connect to the network ----------------------
const client = new Client('wss://s.devnet.rippletest.net:51233')
await client.connect()

const EXPLORER = 'https://devnet.xrpl.org'
const ticker = 'CTST'
const supplyAmount = 12000
```

Python
- `json`: Used to write the generated accounts and keys to a local file.
- `xrpl`: Used for XRPL client connection, transaction submission, and wallet handling.
- `xrpl.ext.confidential`: Used for key generation, confidential transaction builders, and decryption.


```py
import json

from xrpl.clients import JsonRpcClient
from xrpl.ext.confidential import (
    MPTCrypto,
    decrypt_confidential_balance,
    prepare_confidential_convert,
    prepare_confidential_merge_inbox,
)
from xrpl.models import (
    LedgerEntry,
    MPTokenAuthorize,
    MPTokenIssuanceCreate,
    MPTokenIssuanceCreateFlag,
    MPTokenIssuanceSet,
    Payment,
)
from xrpl.models.requests.ledger_entry import MPToken
from xrpl.transaction import submit_and_wait
from xrpl.utils import encode_mptoken_metadata
from xrpl.wallet import generate_faucet_wallet

# Connect to the network ----------------------
client = JsonRpcClient("https://s.devnet.rippletest.net:51234")

EXPLORER = "https://devnet.xrpl.org"
ticker = "CTST"
supply_amount = 12000
```

Next, fund the three accounts this example needs:

- **Issuer**: Creates the MPT issuance and registers the encryption keys.
- **Second account**: A regular holder account that the issuer controls. This is the account that holds the confidential balance.
- **Auditor**: An independent party that can decrypt every holder's balance for this issuance.


JavaScript
```js
// Fund the accounts ----------------------
// An issuer cannot hold a confidential balance on the account that issues the
// token, because an issuer's own balance is not counted as tokens in
// circulation. Confidential tokens enter circulation through a second account
// the issuer controls, which the ledger treats as a regular holder.
console.log(`\n=== Funding accounts... ===`)
const [{ wallet: issuer }, { wallet: issuerSecondAccount }, { wallet: auditor }] =
  await Promise.all([
    client.fundWallet(),
    client.fundWallet(),
    client.fundWallet()
  ])
console.log(`Issuer address: ${issuer.address}`)
console.log(`Issuer second account address: ${issuerSecondAccount.address}`)
console.log(`Auditor address: ${auditor.address}`)
```

Python
```py
# Fund the accounts ----------------------
# An issuer cannot hold a confidential balance on the account that issues the
# token, because an issuer's own balance is not counted as tokens in
# circulation. Confidential tokens enter circulation through a second account
# the issuer controls, which the ledger treats as a regular holder.
print("\n=== Funding accounts... ===")
issuer = generate_faucet_wallet(client)
issuer_second_account = generate_faucet_wallet(client)
auditor = generate_faucet_wallet(client)
print(f"Issuer address: {issuer.address}")
print(f"Issuer second account address: {issuer_second_account.address}")
print(f"Auditor address: {auditor.address}")
```

An issuing account can't hold a confidential balance of its own token, because an issuer's balance doesn't count as tokens in circulation. Confidential tokens enter circulation through a second account that the issuer controls and that the ledger treats as a regular holder.

Note
The auditor account is optional, so include it only if the token needs independent oversight. The auditor key must be registered in the same transaction as the issuer key, so this decision is permanent.

### 3. Generate the encryption keypairs

Confidential transfers use EC-ElGamal encryption over secp256k1. Each participant needs an encryption keypair, which is separate from the account's signing keys. The private key is the only way to decrypt a balance, and the public key is what gets registered on the ledger.

JavaScript
Generate the encryption keypairs with `deriveConfidentialKeypair()`. This helper function derives the keypair deterministically from an account's seed, so one backed-up secret recovers both the signing key and the encryption key.

```js
// Generate confidential encryption keypairs ----------------------
// These ElGamal keypairs are separate from the accounts' signing keys.
console.log(`\n=== Generating confidential encryption keypairs... ===`)
const issuerKeys = deriveConfidentialKeypair(issuer.seed)
const issuerSecondAccountKeys = deriveConfidentialKeypair(issuerSecondAccount.seed)
const auditorKeys = deriveConfidentialKeypair(auditor.seed)
console.log(`Issuer public encryption key: ${issuerKeys.publicKey}`)
console.log(`Second account public encryption key: ${issuerSecondAccountKeys.publicKey}`)
console.log(`Auditor public encryption key: ${auditorKeys.publicKey}`)
```

Python
Generate the encryption keypairs with `generate_keypair()`. This helper function returns a fresh random keypair as a `(private_key, public_key)` tuple. It isn't derived from the account seed, so you have to store the private key yourself.

```py
# Generate confidential encryption keypairs ----------------------
# These ElGamal keypairs are separate from the accounts' signing keys.
print("\n=== Generating confidential encryption keypairs... ===")
crypto = MPTCrypto()
issuer_privkey, issuer_pubkey = crypto.generate_keypair()
issuer_second_account_privkey, issuer_second_account_pubkey = crypto.generate_keypair()
auditor_privkey, auditor_pubkey = crypto.generate_keypair()
print(f"Issuer public encryption key: {issuer_pubkey}")
print(f"Second account public encryption key: {issuer_second_account_pubkey}")
print(f"Auditor public encryption key: {auditor_pubkey}")
```

Warning
**Store the encryption private keys securely.** If a holder loses their encryption private key, their confidential balance is permanently unspendable. The holder can't decrypt it and can't generate the proofs needed to send or convert it back. Registered public keys are permanent and can't be changed or cleared.

### 4. Create the MPT issuance

Submit an [MPTokenIssuanceCreate transaction](/docs/references/protocol/transactions/types/mptokenissuancecreate) with the `tfMPTCanHoldConfidentialBalance` flag. This flag is what makes the issuance eligible for confidential balances.

The example sets `tfMPTCanTransfer` so holders can send the token to each other, `tfMPTCanClawback` so the issuer can recover balances, and `tfMPTCanLock` so the issuer can freeze them.

JavaScript
```js
// Create the MPT issuance ----------------------
console.log(`\n=== Creating the MPT issuance ===`)
const mptMetadata = {
  ticker,
  name: 'Confidential Token',
  desc: 'A confidential demo token.',
  icon: 'https://example.org/ctst-icon.png',
  asset_class: 'rwa',
  asset_subclass: 'treasury',
  issuer_name: 'Example Financial Corp',
  additional_info: {
    interest_rate: '4.25%',
    interest_type: 'fixed'
  }
}

const mptIssuanceCreate = {
  TransactionType: 'MPTokenIssuanceCreate',
  Account: issuer.address,
  AssetScale: 0,
  MaximumAmount: '1000000000',
  TransferFee: 0,
  Flags:
    MPTokenIssuanceCreateFlags.tfMPTCanHoldConfidentialBalance |
    MPTokenIssuanceCreateFlags.tfMPTCanTransfer |
    MPTokenIssuanceCreateFlags.tfMPTCanClawback |
    MPTokenIssuanceCreateFlags.tfMPTCanLock,
  MPTokenMetadata: encodeMPTokenMetadata(mptMetadata)
}

const createResponse = await client.submitAndWait(mptIssuanceCreate, {
  wallet: issuer,
  autofill: true
})
if (createResponse.result.meta.TransactionResult !== 'tesSUCCESS') {
  const resultCode = createResponse.result.meta.TransactionResult
  console.error('Error: Unable to create the MPT issuance:', resultCode)
  await client.disconnect()
  process.exit(1)
}
const mptIssuanceID = createResponse.result.meta.mpt_issuance_id
console.log(`MPT issuance ID: ${mptIssuanceID}`)
console.log(`${EXPLORER}/transactions/${createResponse.result.hash}`)
```

Python
```py
# Create the MPT issuance ----------------------
print("\n=== Creating the MPT issuance ===")
mpt_metadata = {
    "ticker": ticker,
    "name": "Confidential Token",
    "desc": "A confidential demo token.",
    "icon": "https://example.org/ctst-icon.png",
    "asset_class": "rwa",
    "asset_subclass": "treasury",
    "issuer_name": "Example Financial Corp",
    "additional_info": {
        "interest_rate": "4.25%",
        "interest_type": "fixed",
    },
}

mpt_issuance_create = MPTokenIssuanceCreate(
    account=issuer.address,
    asset_scale=0,
    maximum_amount="1000000000",
    transfer_fee=0,
    flags=MPTokenIssuanceCreateFlag.TF_MPT_CAN_HOLD_CONFIDENTIAL_BALANCE
    | MPTokenIssuanceCreateFlag.TF_MPT_CAN_TRANSFER
    | MPTokenIssuanceCreateFlag.TF_MPT_CAN_CLAWBACK
    | MPTokenIssuanceCreateFlag.TF_MPT_CAN_LOCK,
    mptoken_metadata=encode_mptoken_metadata(mpt_metadata),
)

create_response = submit_and_wait(mpt_issuance_create, client, issuer, autofill=True)
if create_response.result["meta"]["TransactionResult"] != "tesSUCCESS":
    result_code = create_response.result["meta"]["TransactionResult"]
    print(f"Error: Unable to create the MPT issuance: {result_code}")
    exit(1)
mpt_issuance_id = create_response.result["meta"]["mpt_issuance_id"]
print(f"MPT issuance ID: {mpt_issuance_id}")
print(f"{EXPLORER}/transactions/{create_response.result['hash']}")
```

You can also add this capability to an existing issuance with the `tfMPTSetCanHoldConfidentialBalance` flag on an [MPTokenIssuanceSet transaction](/docs/references/protocol/transactions/types/mptokenissuanceset), as long as the issuer didn't mark the capability immutable at creation. Enabling it is **one-way**, so there is no flag to turn it off.

Caution
The `TransferFee` must be `0` on an issuance that can hold confidential balances, because the ledger can't compute a percentage fee on an encrypted amount.

### 5. Register the encryption keys

Holders can't convert their balances into confidential ones until the issuer registers an `IssuerEncryptionKey` with an [MPTokenIssuanceSet transaction](/docs/references/protocol/transactions/types/mptokenissuanceset). Register an `AuditorEncryptionKey` in the same transaction if the token needs independent oversight.

JavaScript
```js
// Register the encryption keys on the issuance ----------------------
console.log(`\n=== Registering the encryption keys... ===`)
const mptIssuanceSet = {
  TransactionType: 'MPTokenIssuanceSet',
  Account: issuer.address,
  MPTokenIssuanceID: mptIssuanceID,
  IssuerEncryptionKey: issuerKeys.publicKey,
  AuditorEncryptionKey: auditorKeys.publicKey
}

const setResponse = await client.submitAndWait(mptIssuanceSet, {
  wallet: issuer,
  autofill: true
})
if (setResponse.result.meta.TransactionResult !== 'tesSUCCESS') {
  const resultCode = setResponse.result.meta.TransactionResult
  console.error('Error: Unable to register the encryption keys:', resultCode)
  await client.disconnect()
  process.exit(1)
}
console.log('Issuer and auditor encryption keys registered.')
console.log(`${EXPLORER}/transactions/${setResponse.result.hash}`)
```

Python
```py
# Register the encryption keys on the issuance ----------------------
print("\n=== Registering the encryption keys... ===")
mpt_issuance_set = MPTokenIssuanceSet(
    account=issuer.address,
    mptoken_issuance_id=mpt_issuance_id,
    issuer_encryption_key=issuer_pubkey,
    auditor_encryption_key=auditor_pubkey,
)

set_response = submit_and_wait(mpt_issuance_set, client, issuer, autofill=True)
if set_response.result["meta"]["TransactionResult"] != "tesSUCCESS":
    result_code = set_response.result["meta"]["TransactionResult"]
    print(f"Error: Unable to register the encryption keys: {result_code}")
    exit(1)
print("Issuer and auditor encryption keys registered.")
print(f"{EXPLORER}/transactions/{set_response.result['hash']}")
```

Warning
Key registration is a permanent operation:

- Neither key can be changed or removed after registration. Resubmitting a key that's already on the issuance returns `tecNO_PERMISSION`.
- `AuditorEncryptionKey` requires `IssuerEncryptionKey` in the same transaction, or the transaction returns `temMALFORMED`. Because the issuer key can't be resubmitted, this is the only chance to register an auditor.


Once an auditor is registered, every confidential transaction on the issuance must have an `AuditorEncryptedAmount`.

### 6. Send public tokens to the second account

To introduce confidential tokens into circulation, the issuer must send the required amount to their second account, and convert it.

Authorize the second account to hold the MPT with an [MPTokenAuthorize transaction](/docs/references/protocol/transactions/types/mptokenauthorize), then send it the required amount with a [Payment transaction](/docs/references/protocol/transactions/types/payment).

JavaScript
```js
// Authorize the second account ----------------------
console.log(`\n=== Authorizing the issuer second account... ===`)
const mptAuthorize = {
  TransactionType: 'MPTokenAuthorize',
  Account: issuerSecondAccount.address,
  MPTokenIssuanceID: mptIssuanceID
}

const authorizeResponse = await client.submitAndWait(mptAuthorize, {
  wallet: issuerSecondAccount,
  autofill: true
})
if (authorizeResponse.result.meta.TransactionResult !== 'tesSUCCESS') {
  const resultCode = authorizeResponse.result.meta.TransactionResult
  console.error('Error: Unable to authorize the second account:', resultCode)
  await client.disconnect()
  process.exit(1)
}
console.log(`${issuerSecondAccount.address} is authorized to hold the MPT.`)
console.log(`${EXPLORER}/transactions/${authorizeResponse.result.hash}`)

// Send public tokens to the second account ----------------------
console.log(`\n=== Sending public tokens to the second account... ===`)
const payment = {
  TransactionType: 'Payment',
  Account: issuer.address,
  Destination: issuerSecondAccount.address,
  Amount: {
    mpt_issuance_id: mptIssuanceID,
    value: String(supplyAmount)
  }
}

const paymentResponse = await client.submitAndWait(payment, {
  wallet: issuer,
  autofill: true
})
if (paymentResponse.result.meta.TransactionResult !== 'tesSUCCESS') {
  const resultCode = paymentResponse.result.meta.TransactionResult
  console.error('Error: Unable to send the payment:', resultCode)
  await client.disconnect()
  process.exit(1)
}
console.log(`Issuer sent ${supplyAmount} ${ticker} to ${issuerSecondAccount.address}.`)
console.log(`${EXPLORER}/transactions/${paymentResponse.result.hash}`)
```

Python
```py
# Authorize the second account ----------------------
print("\n=== Authorizing the issuer second account... ===")
mpt_authorize = MPTokenAuthorize(
    account=issuer_second_account.address,
    mptoken_issuance_id=mpt_issuance_id,
)

authorize_response = submit_and_wait(
    mpt_authorize, client, issuer_second_account, autofill=True
)
if authorize_response.result["meta"]["TransactionResult"] != "tesSUCCESS":
    result_code = authorize_response.result["meta"]["TransactionResult"]
    print(f"Error: Unable to authorize the second account: {result_code}")
    exit(1)
print(f"{issuer_second_account.address} is authorized to hold the MPT.")
print(f"{EXPLORER}/transactions/{authorize_response.result['hash']}")

# Send public tokens to the second account ----------------------
print("\n=== Sending public tokens to the second account... ===")
payment = Payment(
    account=issuer.address,
    destination=issuer_second_account.address,
    amount={
        "mpt_issuance_id": mpt_issuance_id,
        "value": str(supply_amount),
    },
)

payment_response = submit_and_wait(payment, client, issuer, autofill=True)
if payment_response.result["meta"]["TransactionResult"] != "tesSUCCESS":
    result_code = payment_response.result["meta"]["TransactionResult"]
    print(f"Error: Unable to send the payment: {result_code}")
    exit(1)
print(f"Issuer sent {supply_amount} {ticker} to {issuer_second_account.address}.")
print(f"{EXPLORER}/transactions/{payment_response.result['hash']}")
```

Note that this payment is **public**. The amount is visible to anyone reading the ledger, and it sets the ceiling on how much this account can hold confidentially.

### 7. Convert the public balance to a confidential balance

Submit a [ConfidentialMPTConvert transaction](/docs/references/protocol/transactions/types/confidentialmptconvert) to convert the public balance into a confidential one.

Note
Confidential transactions cost 10 times the standard [transaction cost](/es-es/docs/concepts/transactions/transaction-cost).

JavaScript
`prepareConfidentialConvert` reads the issuer's and auditor's registered public keys from the [MPTokenIssuance entry](/docs/references/protocol/ledger-data/ledger-entry-types/mptokenissuance), so you pass only the holder's own keypair.

```js
// Convert the public balance to a confidential balance ----------------------
console.log(`\n=== Converting public balance to confidential... ===`)
const convertTx = await prepareConfidentialConvert(client, {
  account: issuerSecondAccount.address,
  mptIssuanceID,
  amount: BigInt(supplyAmount),
  holderKeypair: issuerSecondAccountKeys
})
console.log(JSON.stringify(convertTx, null, 2))

const convertResponse = await client.submitAndWait(convertTx, {
  wallet: issuerSecondAccount,
  autofill: true
})
if (convertResponse.result.meta.TransactionResult !== 'tesSUCCESS') {
  const resultCode = convertResponse.result.meta.TransactionResult
  console.error('Error: Unable to convert the balance:', resultCode)
  await client.disconnect()
  process.exit(1)
}
console.log(`Converted ${supplyAmount} ${ticker} into a confidential balance.`)
console.log(`${EXPLORER}/transactions/${convertResponse.result.hash}`)
```

Python
`prepare_confidential_convert` takes the issuer's and the auditor's registered public keys as arguments, along with the holder's own keypair.

```py
# Convert the public balance to a confidential balance ----------------------
print("\n=== Converting public balance to confidential... ===")
convert_tx = prepare_confidential_convert(
    client,
    issuer_second_account,
    mpt_issuance_id,
    supply_amount,
    issuer_pubkey,
    issuer_second_account_privkey,
    issuer_second_account_pubkey,
    auditor_pubkey,
)
print(json.dumps(convert_tx.to_dict(), indent=2))

convert_response = submit_and_wait(
    convert_tx, client, issuer_second_account, autofill=True
)
if convert_response.result["meta"]["TransactionResult"] != "tesSUCCESS":
    result_code = convert_response.result["meta"]["TransactionResult"]
    print(f"Error: Unable to convert the balance: {result_code}")
    exit(1)
print(f"Converted {supply_amount} {ticker} into a confidential balance.")
print(f"{EXPLORER}/transactions/{convert_response.result['hash']}")
```

The helper function handles the following for you:

- Encrypts the amount under the holder's, the issuer's, and any auditor's public keys, using one shared blinding factor so every ciphertext commits to the same value.
- Generates a Schnorr proof that the account controls the encryption key it registers.
- Sets a `HolderEncryptionKey`, so the holder's first conversion doubles as their opt-in. No separate registration step is needed.


Caution
The Schnorr proof commits to the account's `Sequence` number as the helper function read it from the ledger. Submit the prepared transaction before sending anything else from that account. If another transaction lands first, the prepared transaction is no longer valid and must be rebuilt.

Note that the `MPTAmount` field on a conversion is plaintext. Observers can see how much moved into the confidential pool, but not how it's distributed or spent afterwards.

### 8. Merge the inbox into the spending balance

A confidential balance has two buckets on the [MPToken entry](/docs/references/protocol/ledger-data/ledger-entry-types/mptoken):

- `ConfidentialBalanceInbox` receives incoming funds, from both conversions and confidential payments.
- `ConfidentialBalanceSpending` is the only bucket a [ConfidentialMPTSend transaction](/docs/references/protocol/transactions/types/confidentialmptsend) can draw from.


Submit a [ConfidentialMPTMergeInbox transaction](/docs/references/protocol/transactions/types/confidentialmptmergeinbox) to move the inbox balance into the spending balance. The two buckets exist so that an incoming payment can't invalidate a proof that the holder is already building against their spending balance.

JavaScript
```js
// Merge the inbox into the spending balance ----------------------
// A conversion lands in the inbox balance. Merging folds it into the spending
// balance, which is the only balance a confidential send can draw from.
console.log(`\n=== Merging inbox into spending balance... ===`)
const mergeTx = await prepareConfidentialMergeInbox(client, {
  account: issuerSecondAccount.address,
  mptIssuanceID
})
console.log(JSON.stringify(mergeTx, null, 2))

const mergeResponse = await client.submitAndWait(mergeTx, {
  wallet: issuerSecondAccount,
  autofill: true
})
if (mergeResponse.result.meta.TransactionResult !== 'tesSUCCESS') {
  const resultCode = mergeResponse.result.meta.TransactionResult
  console.error('Error: Unable to merge the inbox:', resultCode)
  await client.disconnect()
  process.exit(1)
}
console.log('Inbox merged into the spending balance.')
console.log(`${EXPLORER}/transactions/${mergeResponse.result.hash}`)
```

Python
```py
# Merge the inbox into the spending balance ----------------------
# A conversion lands in the inbox balance. Merging folds it into the spending
# balance, which is the only balance a confidential send can draw from.
print("\n=== Merging inbox into spending balance... ===")
merge_tx = prepare_confidential_merge_inbox(
    client, issuer_second_account, mpt_issuance_id
)
print(json.dumps(merge_tx.to_dict(), indent=2))

merge_response = submit_and_wait(merge_tx, client, issuer_second_account, autofill=True)
if merge_response.result["meta"]["TransactionResult"] != "tesSUCCESS":
    result_code = merge_response.result["meta"]["TransactionResult"]
    print(f"Error: Unable to merge the inbox: {result_code}")
    exit(1)
print("Inbox merged into the spending balance.")
print(f"{EXPLORER}/transactions/{merge_response.result['hash']}")
```

Each merge increments the holder's `ConfidentialBalanceVersion`. Proofs for confidential sends are bound to that version, so a merge invalidates any proof that was built against a previous one.

### 9. Decrypt the confidential balance

The second account is controlled by the issuer, so the issuer holds its encryption private key. Read the second account's `MPToken` entry, then decrypt `ConfidentialBalanceSpending` with that key to confirm the conversion was successful.

JavaScript
```js
// Decrypt the confidential balance ----------------------
console.log(`\n=== Decrypting the confidential balance... ===`)
const [mptoken, mptIssuance] = await Promise.all([
  fetchMPToken(client, issuerSecondAccount.address, mptIssuanceID),
  fetchMPTokenIssuance(client, mptIssuanceID)
])

// The entry carries the same balance once per registered key, so each party
// reads it with their own private key.
console.log(`MPToken entry:`)
console.log(JSON.stringify(mptoken, null, 2))

const crypto = await loadMptCrypto()
const confidentialSupply = BigInt(mptIssuance.ConfidentialOutstandingAmount)

// Only a party holding the matching private key can decrypt the balance.
const secondAccountBalance = await crypto.decryptAmount(
  mptoken.ConfidentialBalanceSpending,
  issuerSecondAccountKeys.privateKey,
  confidentialSupply
)
console.log(`\nSecond account reads its balance as: ${secondAccountBalance} ${ticker}`)
```

Python
```py
# Decrypt the confidential balance ----------------------
print("\n=== Decrypting the confidential balance... ===")
mptoken = client.request(
    LedgerEntry(
        mptoken=MPToken(
            mpt_issuance_id=mpt_issuance_id,
            account=issuer_second_account.address,
        ),
    )
).result["node"]
issuance = client.request(
    LedgerEntry(mpt_issuance=mpt_issuance_id),
).result["node"]

# The entry carries the same balance once per registered key, so each party
# reads it with their own private key.
print("MPToken entry:")
print(json.dumps(mptoken, indent=2))

confidential_supply = int(issuance["ConfidentialOutstandingAmount"])

# Only a party holding the matching private key can decrypt the
# balance.
issuer_second_account_balance = decrypt_confidential_balance(
    mptoken["ConfidentialBalanceSpending"],
    issuer_second_account_privkey,
    range_high=confidential_supply,
)
print(
    f"\nSecond account reads its balance as: "
    f"{issuer_second_account_balance} {ticker}"
)
```

The `MPToken` also has an `AuditorEncryptedBalance` field, which holds the same amount, but is encrypted under the auditor key. The auditor can decrypt it with its own private key and should see the same balance, without ever needing the issuer's key.

JavaScript
```js
// The auditor reads the same amount from a separate ciphertext on the same
// entry, using its own private key.
const auditorView = await crypto.decryptAmount(
  mptoken.AuditorEncryptedBalance,
  auditorKeys.privateKey,
  confidentialSupply
)
console.log(`Auditor reads the balance as: ${auditorView} ${ticker}`)
```

Python
```py
# The auditor reads the same amount from a separate ciphertext on the same
# entry, using its own private key.
auditor_view = decrypt_confidential_balance(
    mptoken["AuditorEncryptedBalance"],
    auditor_privkey,
    range_high=confidential_supply,
)
print(f"Auditor reads the balance as: {auditor_view} {ticker}")
```

Everyone else sees only the ciphertext.

### 10. Save the accounts and keys

The example writes the account seeds and the encryption keypairs to a `keys.json` file, so you can reuse these accounts.

Warning
**Saving keys to a JSON file is not secure, and is only acceptable when working on a test network.** In production, store them in a secure, encrypted key store.

JavaScript
```js
// Save the accounts and keys ----------------------
// Losing an encryption private key makes a confidential balance permanently
// unspendable, so write the seeds and keypairs to keys.json.
console.log(`\n=== Saving accounts and keys to keys.json... ===`)
const keysData = {
  description:
    'This file is auto-generated by issueConfidentialMPT.js. It stores the account seeds and confidential encryption keypairs that script created.',
  issuer: {
    seed: issuer.seed,
    privateKey: issuerKeys.privateKey,
    publicKey: issuerKeys.publicKey
  },
  issuerSecondAccount: {
    seed: issuerSecondAccount.seed,
    privateKey: issuerSecondAccountKeys.privateKey,
    publicKey: issuerSecondAccountKeys.publicKey
  },
  auditor: {
    seed: auditor.seed,
    privateKey: auditorKeys.privateKey,
    publicKey: auditorKeys.publicKey
  }
}

fs.writeFileSync('keys.json', JSON.stringify(keysData, null, 2))
console.log('Saved keys to file.')

await client.disconnect()
```

Python
```py
# Save the accounts and keys ----------------------
# MPTCrypto.generate_keypair is not derived from the account seed, and losing an
# encryption private key makes a confidential balance permanently unspendable,
# so write the seeds and keypairs to keys.json.
print("\n=== Saving accounts and keys to keys.json... ===")
keys_data = {
    "description": (
        "This file is auto-generated by issue_confidential_mpt.py. It stores "
        "the account seeds and confidential encryption keypairs that script "
        "created."
    ),
    "issuer": {
        "seed": issuer.seed,
        "privateKey": issuer_privkey,
        "publicKey": issuer_pubkey,
    },
    "issuerSecondAccount": {
        "seed": issuer_second_account.seed,
        "privateKey": issuer_second_account_privkey,
        "publicKey": issuer_second_account_pubkey,
    },
    "auditor": {
        "seed": auditor.seed,
        "privateKey": auditor_privkey,
        "publicKey": auditor_pubkey,
    },
}

with open("keys.json", "w") as keys_file:
    json.dump(keys_data, keys_file, indent=2)
print("Saved keys to file.")
```

## See Also

- **Concepts**:
  - [Confidential Transfers](/es-es/docs/concepts/tokens/fungible-tokens/confidential-transfers)
  - [Multi-Purpose Tokens (MPT)](/es-es/docs/concepts/tokens/fungible-tokens/multi-purpose-tokens)
- **Tutorials**:
  - [Send Confidential MPT Payments](/es-es/docs/tutorials/payments/send-confidential-payments)
- **References**:
  - [ConfidentialMPTConvert transaction](/docs/references/protocol/transactions/types/confidentialmptconvert)
  - [ConfidentialMPTMergeInbox transaction](/docs/references/protocol/transactions/types/confidentialmptmergeinbox)
  - [MPToken entry](/docs/references/protocol/ledger-data/ledger-entry-types/mptoken)
  - [MPTokenIssuance entry](/docs/references/protocol/ledger-data/ledger-entry-types/mptokenissuance)
  - [MPTokenIssuanceCreate transaction](/docs/references/protocol/transactions/types/mptokenissuancecreate)
  - [MPTokenIssuanceSet transaction](/docs/references/protocol/transactions/types/mptokenissuanceset)