lssa/LEZ testnet v0.1 tutorials/token-transfer.md
2026-02-05 15:24:47 +01:00

9.5 KiB
Raw Blame History

This tutorial walks through native token transfers between public and private accounts using the Authenticated-Transfers program. You will create and initialize accounts, fund them with the Pinata program, and run transfers across different privacy combinations. By the end, you will have practiced:

  1. Public account creation and initialization.
  2. Account funding through the Pinata program.
  3. Native token transfers between public accounts.
  4. Private account creation.
  5. Native token transfer from a public account to a private account.
  6. Native token transfer from a public account to a private account owned by someone else.

The CLI provides commands to manage accounts. Run wallet account to see the options available:

Commands:
  get           Get account data
  new           Produce new public or private account
  sync-private  Sync private accounts
  help  Print this message or the help of the given subcommand(s)

1. Public account creation and initialization

Important

Public accounts live on-chain and are identified by a 32-byte Account ID. Running wallet account new public generates a fresh keypair for the signature scheme used in LEZ. The account ID is derived from the public key, and the private key signs transactions and authorizes program executions. The CLI can create both public and private accounts.

a. New public account creation

wallet account new public

# Output:
Generated new account with account_id Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ

Tip

Save this account ID. You will use it in later commands.

b. Account initialization

To query the accounts current status, run:

# Replace the id with yours
wallet account get --account-id Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ

# Output:
Account is Uninitialized

In this example, we initialize the account for the authenticated-transfer program, which manages native token transfers and enforces authenticated debits.

  1. Initialize the account:
# This command submits a public transaction executing the `init` function of the
# authenticated-transfer program. The wallet polls the sequencer until the
# transaction is included in a block, which may take several seconds.
wallet auth-transfer init --account-id Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ
  1. Check the updated account status:
wallet account get --account-id Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ

# Output:
Account owned by authenticated-transfer program
{"balance":0}

Note

New accounts start uninitialized, meaning no program owns them yet. Any program may claim an uninitialized account; once claimed, that program owns it. Owned accounts can only be modified through executions of the owning program. The only exception is native-token credits: any program may credit native tokens to any account. Debiting native tokens must always be performed by the owning program.

2. Account funding through the Piñata program

Now that the account is initialized under the authenticated-tansfer program, fund it using the testnet Piñata program.

# Replace with your id
wallet pinata claim --to Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ

After the claim succeeds, the account is funded:

wallet account get --account-id Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ

# Output:
Account owned by authenticated-transfer program
{"balance":150}

3. Native token transfers between public accounts

LEZ includes a program for managing native tokens. Run wallet auth-transfer to see the available commands:

Commands:
  init  Initialize account under the authenticated-transfer program
  send  Send native tokens from one account to another with variable privacy
  help  Print this message or the help of the given subcommand(s)

We already used init. Now use send to execute a transfer.

a. Create a recipient account

wallet account new public

# Output:
Generated new account with account_id Public/Ev1JprP9BmhbFVQyBcbznU8bAXcwrzwRoPTetXdQPAWS

Note

The new account is uninitialized. The authenticated-transfer program will claim any uninitialized account used in a transfer, so manual initialization isnt required.

b. Send 37 tokens to the new account

wallet auth-transfer send \
    --from Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ \
    --to Public/Ev1JprP9BmhbFVQyBcbznU8bAXcwrzwRoPTetXdQPAWS \
    --amount 37

c. Check both accounts

# Sender account (use your sender ID)
wallet account get --account-id Public/HrA8TVjBS8UVf9akV7LRhyh6k4c7F6PS7PvqgtPmKAT8

# Output:
Account owned by authenticated-transfer program
{"balance":113}
# Recipient account
wallet account get --account-id Public/Ev1JprP9BmhbFVQyBcbznU8bAXcwrzwRoPTetXdQPAWS

# Output:
Account owned by authenticated-transfer program
{"balance":37}

4. Private account creation

Important

Private accounts are structurally identical to public accounts, but their values are stored off-chain. On-chain, only a 32-byte commitment is recorded. Transactions include encrypted private values so the owner can recover them, and the decryption keys are never shared. Private accounts use two keypairs: nullifier keys for privacy-preserving executions and viewing keys for encrypting and decrypting values. The private account ID is derived from the nullifier public key. Private accounts can be initialized by anyone, but once initialized they can only be modified by the owners keys. Updates include a new commitment and a nullifier for the old state, which prevents linkage between versions.

a. Create a private account

wallet account new private

# Output:
Generated new account with account_id Private/HacPU3hakLYzWtSqUPw6TUr8fqoMieVWovsUR6sJf7cL
With npk e6366f79d026c8bd64ae6b3d601f0506832ec682ab54897f205fffe64ec0d951
With ipk 02ddc96d0eb56e00ce14994cfdaec5ae1f76244180a919545983156e3519940a17

Tip

Focus on the account ID for now. The npk and ipk values are stored locally and used to build privacy-preserving transactions. The private account ID is derived from npk.

Just like public accounts, new private accounts start out uninitialized:

wallet account get --account-id Private/HacPU3hakLYzWtSqUPw6TUr8fqoMieVWovsUR6sJf7cL

# Output:
Account is Uninitialized

Important

Private accounts are never visible to the network. They exist only in your local wallet storage.

5. Native token transfer from a public account to a private account

Important

Sending tokens to an uninitialized private account causes the authenticated-transfer program to claim it, just like with public accounts. Program logic is the same regardless of account type.

a. Send 17 tokens to the private account

Note

The syntax matches public-to-public transfers, but the recipient is a private ID. This runs locally, generates a proof, and submits it to the sequencer. It may take 30 seconds to 4 minutes.

wallet auth-transfer send \
    --from Public/Ev1JprP9BmhbFVQyBcbznU8bAXcwrzwRoPTetXdQPAWS \
    --to Private/HacPU3hakLYzWtSqUPw6TUr8fqoMieVWovsUR6sJf7cL \
    --amount 17

b. Check both accounts

# Public sender account
wallet account get --account-id Public/Ev1JprP9BmhbFVQyBcbznU8bAXcwrzwRoPTetXdQPAWS

# Output:
Account owned by authenticated-transfer program
{"balance":20}
# Private recipient account
wallet account get --account-id Private/HacPU3hakLYzWtSqUPw6TUr8fqoMieVWovsUR6sJf7cL

# Output:
Account owned by authenticated-transfer program
{"balance":17}

Note

The last command does not query the network. It works offline because private account data is stored locally. Other users cannot read your private balances.

Caution

Private accounts can only be modified by their owners keys. The exception is initialization: any user can initialize an uninitialized private account. This enables transfers to a private account owned by someone else, as long as that account is uninitialized.

6. Native token transfer from a public account to a private account owned by someone else

Important

Well simulate transferring to someone else by creating a new private account we own and treating it as if it belonged to another user.

a. Create a new uninitialized private account

wallet account new private

# Output:
Generated new account with account_id Private/AukXPRBmrYVqoqEW2HTs7N3hvTn3qdNFDcxDHVr5hMm5
With npk 0c95ebc4b3830f53da77bb0b80a276a776cdcf6410932acc718dcdb3f788a00e
With ipk 039fd12a3674a880d3e917804129141e4170d419d1f9e28a3dcf979c1f2369cb72

Tip

Ignore the private account ID here and use the npk and ipk values to send to a foreign private account.

wallet auth-transfer send \
    --from Public/Ev1JprP9BmhbFVQyBcbznU8bAXcwrzwRoPTetXdQPAWS \
    --to-npk 0c95ebc4b3830f53da77bb0b80a276a776cdcf6410932acc718dcdb3f788a00e \
    --to-ipk 039fd12a3674a880d3e917804129141e4170d419d1f9e28a3dcf979c1f2369cb72 \
    --amount 3

Warning

This command creates a privacy-preserving transaction, which may take a few minutes. The updated values are encrypted and included in the transaction. Once accepted, the recipient must run wallet account sync-private to scan the chain for their encrypted updates and refresh local state.

Note

You have seen transfers between two public accounts and from a public sender to a private recipient. Transfers from a private sender, whether to a public account or to another private account, follow the same pattern.