Aleo support is embedded-only today. The methods below are exposed on wallet.* for
Dynamic embedded (MPC) Aleo wallets and route through the iframe (the Aleo Feemaster +
Provable’s Decentralized Proving Service). Wallet Adapter Aleo wallets expose
a separate wallet.requestTransaction / wallet.requestRecords API and will throw if
these embedded-only methods are called.
Aleo is a privacy-first L1 with two parallel state types:
- Public — balances tracked on-chain like a standard ledger.
- Private — balances held inside owned records (encrypted UTXOs). Spending a record reveals
nothing about the rest of the wallet’s state.
The Dynamic Aleo wallet exposes operations across both states. All embedded-only operations
sign through MPC and submit through the Provable DPS so the user’s view key never leaves the
iframe.
Installation
Setup
Include the Aleo wallet connector in your provider configuration:
import { DynamicContextProvider } from '@dynamic-labs/sdk-react-core';
import { AleoWalletConnectors } from '@dynamic-labs/aleo';
<DynamicContextProvider
settings={{
walletConnectors: [AleoWalletConnectors],
// ... other settings
}}
>
{/* Your app components */}
</DynamicContextProvider>;
Checking if a wallet is an Aleo wallet
import { isAleoWallet } from '@dynamic-labs/aleo';
if (!isAleoWallet(wallet)) {
throw new Error('This wallet is not an Aleo wallet');
}
Sign a message
Available on every Aleo wallet (embedded and Wallet Adapter):
import { useDynamicContext } from '@dynamic-labs/sdk-react-core';
import { isAleoWallet } from '@dynamic-labs/aleo';
const { primaryWallet } = useDynamicContext();
if (!primaryWallet || !isAleoWallet(primaryWallet)) {
throw new Error('This wallet is not an Aleo wallet');
}
const signature = await primaryWallet.signMessage('hello aleo');
Fetch private balances
For a per-token aggregated view of the wallet’s private balances (credits, stablecoins, ARC-21), use the usePrivateTokenBalances hook. It mirrors useTokenBalances and is the recommended way to render shielded balances next to public ones:
import {
useTokenBalances,
usePrivateTokenBalances,
} from '@dynamic-labs/sdk-react-core';
const { tokenBalances: publicBalances } = useTokenBalances();
const { tokenBalances: privateBalances, supportsPrivateBalances } =
usePrivateTokenBalances();
Underneath the hook reads the wallet’s owned records through the iframe; the view key never leaves the iframe. See the hook reference for the full return shape and a composition example.
List owned records
If you need the raw owned records (rather than the per-token aggregation that usePrivateTokenBalances returns), call listOwnedRecords directly. Each entry carries program_name and record_name so you can group/display by token. Backed by Provable’s RecordScanner via the iframe — the view key never leaves the iframe.
import { useDynamicContext } from '@dynamic-labs/sdk-react-core';
import { isAleoWallet } from '@dynamic-labs/aleo';
const { primaryWallet } = useDynamicContext();
if (!primaryWallet || !isAleoWallet(primaryWallet)) {
throw new Error('This wallet is not an Aleo wallet');
}
const { records } = await primaryWallet.listOwnedRecords();
// records[i].program_name → e.g. 'credits.aleo'
// records[i].record_name → e.g. 'credits' or 'Token'
// records[i].record_plaintext → encrypted record plaintext
Execute a program transition (proveTransaction)
The generic Aleo entry point: signs through MPC, optionally injects a Sealance freeze-list
proof for Sealance-compliant stablecoins, applies Feemaster sponsorship when covered, and
submits to Provable DPS.
import { useDynamicContext } from '@dynamic-labs/sdk-react-core';
import { isAleoWallet } from '@dynamic-labs/aleo';
const { primaryWallet } = useDynamicContext();
if (!primaryWallet || !isAleoWallet(primaryWallet)) {
throw new Error('This wallet is not an Aleo wallet');
}
const { txId } = await primaryWallet.proveTransaction({
programId: 'credits.aleo',
functionName: 'transfer_public',
inputs: ['aleo1recipient...', '1000000u64'],
inputTypes: ['address.public', 'u64.public'],
broadcast: true,
});
Common transitions
credits.aleo/transfer_public(receiver: address, amount: u64) — public→public credits transfer.await primaryWallet.proveTransaction({
programId: 'credits.aleo',
functionName: 'transfer_public',
inputs: [recipient, `${amount.toString()}u64`],
inputTypes: ['address.public', 'u64.public'],
broadcast: true,
});
credits.aleo/transfer_private(record, receiver, amount) — record-to-record private send.await primaryWallet.proveTransaction({
programId: 'credits.aleo',
functionName: 'transfer_private',
inputs: [recordPlaintext, recipient, `${amount.toString()}u64`],
inputTypes: ['credits.record', 'address.private', 'u64.private'],
broadcast: true,
});
credits.aleo/transfer_private_to_public(record, receiver, amount) — spend a record, credit the recipient’s public balance.await primaryWallet.proveTransaction({
programId: 'credits.aleo',
functionName: 'transfer_private_to_public',
inputs: [recordPlaintext, recipient, `${amount.toString()}u64`],
inputTypes: ['credits.record', 'address.public', 'u64.public'],
broadcast: true,
});
credits.aleo/transfer_public_to_private(receiver, amount) — debit the sender’s public balance, mint a private record owned by receiver.The receiver gets a private record so the recipient address is address.private; the
debited amount is public so it’s u64.public. Stablecoin / ARC-21 variants take
u128.public (and ARC-21 additionally takes token_id + external_auth_required).await primaryWallet.proveTransaction({
programId: 'credits.aleo',
functionName: 'transfer_public_to_private',
inputs: [recipient, `${amount.toString()}u64`],
inputTypes: ['address.private', 'u64.public'],
broadcast: true,
});
For the common case of shielding to self (with Feemaster sponsorship checks and
per-token registry handling), prefer the shieldToken
helper below — it wraps this transition.
Shield a token (public → private)
The shield operation maps a public balance into a fresh private record owned by the user. The
helpers below let you check eligibility and Feemaster sponsorship before dispatching.
import { useDynamicContext } from '@dynamic-labs/sdk-react-core';
import { isAleoWallet } from '@dynamic-labs/aleo';
const { primaryWallet } = useDynamicContext();
if (!primaryWallet || !isAleoWallet(primaryWallet)) {
throw new Error('This wallet is not an Aleo wallet');
}
// 1. Is this token registered as shieldable on the current Aleo network?
const canShield = primaryWallet.canShieldToken({
address: 'credits.aleo',
isNative: true,
});
// 2. Does the Aleo Feemaster currently sponsor the shield call?
const sponsored = await primaryWallet.isShieldSponsored({
address: 'credits.aleo',
isNative: true,
});
// 3. Dispatch the shield. Recipient is always self.
if (canShield) {
const txId = await primaryWallet.shieldToken({
tokenAddress: 'credits.aleo',
isNative: true,
amount: 1_000_000n, // atomic units (microcredits for credits, 10^decimals for SPL-style tokens)
});
}
The SDK applies optimistic updates the moment shieldToken resolves: the public balance is
debited and a synthesised record is added to the private balance, so the UI reacts immediately
without waiting for the indexer. Both useTokenBalances
and usePrivateTokenBalances reflect the
optimistic state, and reconcile silently once the new record is indexed.
Merge owned records (joinRecords)
Pairwise-merges owned records down to one per program by recursively running
<program>/join. Sponsored by Feemaster for credits.aleo today; stablecoin / ARC-21 fall
through to user-paid if Feemaster doesn’t yet cover the pair.
Programs the connector doesn’t recognise (no registered join shape) are reported as
skipped: 'unsupported' in results rather than thrown — iterate the results to surface
what merged vs. what didn’t.
import { useDynamicContext } from '@dynamic-labs/sdk-react-core';
import { isAleoWallet } from '@dynamic-labs/aleo';
const { primaryWallet } = useDynamicContext();
if (!primaryWallet || !isAleoWallet(primaryWallet)) {
throw new Error('This wallet is not an Aleo wallet');
}
// Merge every owned program that has ≥2 records.
const { results } = await primaryWallet.joinRecords();
// …or scope to a specific program.
await primaryWallet.joinRecords({ programIds: ['credits.aleo'] });
// …or ARC-21 with a token_id filter (records of different token_ids cannot be merged).
await primaryWallet.joinRecords({
programIds: ['token_registry.aleo'],
filterRecord: (r) => r.record_plaintext?.includes(`token_id: ${tokenId}`),
});
For any (programId, functionName) pair on the currently-selected Aleo network. Useful to
decide whether to dispatch silently or surface a user-paid fee confirmation. Never throws —
returns false on any failure so callers can default to “show modal”.
const sponsored = await primaryWallet.isFeemasterSponsored({
programId: 'credits.aleo',
functionName: 'join',
});
The Dynamic Widget’s “Send” button on Aleo wallets is driven by createUiTransaction, which
exposes two modes:
- Individual —
<program>/transfer_private (record-to-record).
- Exchange — orchestrated two-step:
transfer_private_to_public(record, SELF, amount) — unshield to the user’s own public
balance.
transfer_public(EXCHANGE, amount) — public→public to the exchange recipient.
Auto-shield is paused for the in-flight token during the Exchange flow, so the just-
unshielded amount isn’t immediately re-shielded by the background optimisation.
Resources