Shadow Accounts

Shadow accounts give one user a persistent, pseudonymous identity for one dapp. They are useful for positions that live across transactions - staking, lending, rewards, and any protocol that needs to recognize a returning caller.

This page follows Privacy SDK 0.14.3-rc.8. The old subaccounts(...) API was renamed and is not compatible with the current anonymizer. Use shadowAccounts(...) throughout.

Snippets assume transfers, account and provider from Getting Started.

Configure the anonymizer#

Pass the deployment for the connected network when creating transfers:

const transfers = createPrivateTransfers({
  account,
  viewingKeyProvider,
  provingProvider,
  discoveryProvider,
  poolContractAddress: process.env.POOL_ADDRESS!,
  shadowAccountAnonymizerAddress: process.env.SHADOW_ACCOUNT_ANONYMIZER_ADDRESS!,
})

Calling shadowAccounts(...) without shadowAccountAnonymizerAddress throws. Use the current Mainnet or Sepolia value from Deployed Contract Addresses.

Commitments and addresses#

The builder derives the partial and full commitments locally from the user, viewing key, anonymizer, dapp name, and nonce:

import { shadowAccountAddress } from "@starkware-libs/starknet-privacy-sdk"

const shadow = transfers.build().shadowAccounts("myDapp")
const partial = await shadow.partialCommitment()
const commitment = await shadow.commitment(0n)

const address = shadowAccountAddress(
  commitment,
  BigInt(process.env.SHADOW_ACCOUNT_ANONYMIZER_ADDRESS!),
)

shadowAccountAddress uses the exported, fixed PRIMER_CLASS_HASH, matching the current Cairo deployment pattern. It works before the account is deployed and does not require an RPC call. The package also exports the lower-level shadowAccountPartialCommitment and shadowAccountCommitment helpers when you already hold the raw felts.

Invoke through the shadow account#

Fund the shadow address, create an open note for each expected output token, then queue the calls. The SDK translates this into one ComputeAndInvoke against the ShadowAccountAnonymizer.

import { Open } from "@starkware-libs/starknet-privacy-sdk"

const provingBlockId = (await provider.getBlockNumber()) - 10

const { callAndProof } = await transfers
  .build({ autoDiscover: { notes: "refresh" } })
  .with(STRK)
  .withdraw({ recipient: address, amount: 5n * 10n ** 18n })
  .surplusTo(account.address, false)
  .with(STRK)
  .transfer({ recipient: account.address, amount: Open })
  .done()
  .shadowAccounts("myDapp")
  .invoke(0n, {
    calls: [stakingContract.populate("stake", { amount: 1000n })],
    collectPolicy: { type: "all" },
  })
  .execute({ provingBlockId })

// Submit callAndProof with the usual proofDetails + tip: 0n tail.

collectPolicy is optional in the SDK and defaults to { type: "all" }.

collectPolicy Amount collected into each open note
{ type: "all" } The token's entire shadow-account balance
{ type: "diff" } Only the balance gained during this interaction
{ type: "exact", amount } Exactly amount

Read deployed and undeployed accounts#

The exported ShadowAccountAnonymizerABI includes these views:

  • get_shadow_accounts(partial, start, end, until_undeployed) resolves a nonce range and returns { nonce, address, is_deployed } for each entry.
  • get_shadow_account(commitment) returns the stored address of an already deployed account, or zero when it has not been deployed.
  • get_shadow_account_class_hash() returns the class installed after the primer deployment. It is not the class hash used for address derivation.

The scan range is limited to 1,024 nonces. With until_undeployed: true, the range stops at the first address that has not been deployed.

Things to notice#

  • A shadow account's balances, calls, and positions are public. Privacy comes from hiding the link to the user's main account.
  • A shadow account has no keys. Only the anonymizer can execute through it.
  • One transaction can contain at most one invoke-phase action, whether it is a normal invoke or a shadow-account invocation.
  • One collection policy applies to every open note settled by the invocation.
  • The current anonymizer returns the shadow-account address used by deposits so the pool can apply its screening policy to that address.

Next: Proving Configuration - prepare and submit the resulting call safely.