ZSA Testnet

Ah brill thank you :folded_hands:t2:

Hello everyone wanted to share this testnet work on ZSAs with you all. We exercised a full private custom-asset loop with a test asset we named zUSD:

  1. Fund with faucet tZEC
  2. Scan / decrypt OrchardZSA notes
  3. Issue 100 zUSD (ZIP 227)
  4. Transfer 10 zUSD between accounts (ZIP 226)
  5. Burn 5 zUSD (ZIP 226)

Issue / transfer / burn all mined. Full txids, asset id, and operator notes are in the attached PDF.

Why this matters to us: Zcash already has strong private ZEC. A private dollar-like asset (issue → hold/send shielded → burn) is the missing piece for everyday private payments and a future wrap/redeem path against a public stablecoin. This run is wallet + ZSA plumbing on testnet — not a live redeemable stablecoin, and not mainnet.

Happy to compare notes with other teams building on OrchardZSA.

— NozyWallet / LEONINE-DAO

ZSA_ZUSD_TESTNET_FORUM_POST.pdf (346.1 KB)

I personally never envisioned ZSAs as a way to create stablecoins. Stablecoins are highly regulated, even federally, especially with the passage of the GENIUS Act. Even if you use a USDC->ZSA bidirectional bridge, Circle retains powers to blacklist the bridge and seize the assets at any time by court order or just because they felt like it, rendering every ZSA based on it instantly worthless.

This is true of nearly every stablecoin that is compliant with US law, including USDT, RLUSD (XRP) and more. There are a few remaining alternatives such as AAVE’s SUSD/aSUSD (can still be blacklisted, subject to DAO vote), or Maker DAI, but these do not reliably hold their value and are not backed by any actual money stores.

And I doubt any provider will be willing to spin up a new stablecoin if they can never enforce blacklisting, much less not being able to even view transfers.

TL;DR You can preach about privacy all you want, but if a company faces $1 million in fines per violation and their CEO being arrested when he lands at JFK, it’s not exactly a GOOD prospect for a potential issuer.

Thank you ive seen the test send :folded_hands:t2: thank you

1 Like

I’m gone write a paper about is private digital cash is philosophically wrong.

How to issue & transfer TEST ZSA using AI (no coding)

Environment: macOS (darwin), workdir /Users/hanh, docker+colima (Homebrew),
container zkool-zsa, endpoint http://localhost:8000/graphql


User: “locate the docker container for zkool_graphql on dockerhub”

$ docker search zkool_graphql --no-trunc
Error response from daemon: Get "https://index.docker.io/v1/search?q=zkool_graphql&n=25":
dial tcp: lookup index.docker.io on [::1]:53: read udp [::1]:55541->[::1]:53: read: connection refused
docker CLI not available

# webfetch https://hub.docker.com/v2/search/repositories/?query=zkool_graphql
{"count":0,"next":"","previous":"","results":[]}

# webfetch https://hub.docker.com/v2/search/repositories/?query=zkool
{"count":23,"next":"...page=2...","previous":"","results":[
  {"repo_name":"hhanh00/zkool-graphql","short_description":"","star_count":0,"pull_count":10380,...},
  {"repo_name":"azizkool/kubia",...}, ...]}

# webfetch https://hub.docker.com/v2/repositories/zkool/?page_size=100
{"count":0,"next":null,"previous":null,"results":[]}

# webfetch https://hub.docker.com/v2/repositories/hhanh00/zkool-graphql/tags/?page_size=25
{"count":163,...   [~500 lines of tag JSON truncated]
  tags include: latest (digest sha256:8c98cd5828dddb885fef038e3c7e370604b23715bbd2c634c3e491772cd3b71d),
  6.32.0-rc, 6.31.0, 6.31.0-rc.2, 6.31.0-rc.1, 6.31.0-rc, 6.30.0, 6.30.0-rc.6 ... 6.28.0-rc.11
  multi-arch linux/amd64 + linux/arm64, last push 2026-09-30T12:57:57Z}

Result: image is hhanh00/zkool-graphql.

User: “start docker”

$ open -a Docker && echo "Docker Desktop launching..."
Unable to find application named 'Docker'

$ ls /Applications | grep -i -E "docker|orb|podman" ; which docker colima podman orbstack ; ls ~/Applications | grep -i -E "docker|orb"
/opt/homebrew/bin/docker
/opt/homebrew/bin/colima
podman not found
orbstack not found

$ colima status 2>&1; colima start 2>&1
time="2026-10-04T09:42:55+08:00" level=fatal msg="error retrieving current runtime: empty value"
time="2026-10-04T09:42:56+08:00" level=warning msg="already running, ignoring"

$ docker ps 2>&1 && docker info --format '{{.ServerVersion}} | {{.OperatingSystem}}'
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES
29.2.0 | Ubuntu 24.04.3 LTS

User: “yes” (pull the image)

$ docker pull hhanh00/zkool-graphql:latest 2>&1
Error response from daemon: failed to resolve reference "docker.io/hhanh00/zkool-graphql:latest":
failed to do request: Head "https://registry-1.docker.io/v2/hhanh00/zkool-graphql/manifests/latest":
dial tcp: lookup registry-1.docker.io on [::1]:53: read udp [::1]:36205->[::1]:53: read: connection refused

$ colima restart 2>&1 | tail -5
time="2026-10-04T09:43:53+08:00" level=info msg="provisioning ..." context=docker
colima
Current context is now "colima"
time="2026-10-04T09:43:55+08:00" level=info msg="starting ..." context=docker
time="2026-10-04T09:43:56+08:00" level=info msg=done

$ docker pull hhanh00/zkool-graphql:latest 2>&1
latest: Pulling from hhanh00/zkool-graphql
1403e25654b9: Pulling fs layer
a9986cd6f37d: Pulling fs layer
44136fa355b3: Download complete
a9986cd6f37d: Download complete
2a17294e2185: Download complete
1403e25654b9: Download complete
1403e25654b9: Pull complete
a9986cd6f37d: Pull complete
Digest: sha256:8c98cd5828dddb885fef038e3c7e370604b23715bbd2c634c3e491772cd3b71d
Status: Downloaded newer image for hhanh00/zkool-graphql:latest
docker.io/hhanh00/zkool-graphql:latest

$ docker images hhanh00/zkool-graphql
WARNING: This output is designed for human readability. For machine-readable output, use --format.
IMAGE                          ID             DISK USAGE   CONTENT SIZE   EXTRA
hhanh00/zkool-graphql:latest   8c98cd5828dd        131MB           39MB

User: “run the app with a database named zsa.db connecting to the zsa network at https://zsa.methyl.cc”

$ docker inspect hhanh00/zkool-graphql:latest --format '{{json .Config.Env}} | ENTRYPOINT: {{json .Config.Entrypoint}} | CMD: {{json .Config.Cmd}} | PORTS: {{json .Config.ExposedPorts}} | WORKDIR: {{.Config.WorkingDir}}'
["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"] | ENTRYPOINT: ["/bin/zkool_graphql"] | CMD: null | PORTS: null | WORKDIR: /

# webfetch https://hub.docker.com/r/hhanh00/zkool-graphql → "No overview available"

$ docker run --rm hhanh00/zkool-graphql:latest --help
Usage: zkool_graphql [OPTIONS]

Options:
  -c, --config-path <CONFIG_PATH>
  -d, --db-path <DB_PATH>
  -l, --lwd-url <LWD_URL>
  -p, --port <PORT>
  -n, --no-mempool [<NO_MEMPOOL>]        [possible values: true, false]
  -Z, --zebra [<ZEBRA>]                  Use zebrad/zcashd JSON-RPC backend instead of lightwalletd gRPC...
  -C, --coin <COIN>                      Coin type: 0=mainnet, 1=testnet, 2=regtest, 3=ZSA regtest...
  -j, --jwt-public-key-file <JWT_PUBLIC_KEY_FILE>
  --decode-tx <DECODE_TX>
  -h, --help
exit:0

$ curl -s "https://api.github.com/search/code?q=coin+repo:hhanh00/zkool+filename:main.rs"
{"message":"Requires authentication",...,"status":"401"}
$ curl -s "https://api.github.com/repos/hhanh00/zkool/contents/"
    .gitignore  .metadata  README.md  analysis_options.yaml  android  assets  build.yaml
    flutter_launcher_icons.yaml  ios  l10n.yaml  lib  linux  macos  proto  pubspec.lock  pubspec.yaml

$ curl -s "https://api.github.com/repos/hhanh00/zkool_graphql" | grep -E '"full_name"|"default_branch"|"description"'
(no output)

# Test run (ephemeral, /tmp/zsa.db):
$ docker run --rm --name zsa-test hhanh00/zkool-graphql:latest -d /tmp/zsa.db -l https://zsa.methyl.cc &
$ docker logs zsa-test
2026-10-04T01:46:02.753460Z  WARN zkool_graphql: Server is running WITHOUT authentication. Everyone has full access.
2026-10-04T01:46:02.754063Z  INFO zkool_graphql: Sapling parameters not found, downloading …
2026-10-04T01:46:02.754601Z  INFO rlz::api::sapling: Downloading Sapling parameter sapling-spend.params …
[... first two lines repeated ...]
2026-10-04T01:46:12.553867Z  INFO rlz::api::sapling: Successfully downloaded and verified sapling-spend.params
2026-10-04T01:46:12.559673Z  INFO rlz::api::sapling: Downloading Sapling parameter sapling-output.params …
2026-10-04T01:46:13.919279Z  INFO rlz::api::sapling: Successfully downloaded and verified sapling-output.params
2026-10-04T01:46:13.919708Z  INFO zkool_graphql: Sapling parameters downloaded successfully
2026-10-04T01:46:13.919708Z→ 2026-10-04T01:46:13.919708Z  INFO zkool_graphql: db_path /tmp/zsa.db lwd_url https://zsa.methyl.cc port 8000 zebra false
2026-10-04T01:46:14.010300Z  INFO zkool_graphql: Listening on 0.0.0.0:8000
2026-10-04T01:46:16.017113Z  INFO rlz::graphql::frost: new_block 191804
[container stopped & removed]

$ docker run --rm --entrypoint /bin/sh hhanh00/zkool-graphql:latest -c "ls -la /root ...; which sh"
/root is empty (total 8, . and .. only)
/bin/sh

$ mkdir -p ~/zkool-graphql && docker run -d --name zkool-zsa --restart unless-stopped -p 8000:8000 \
    -v ~/zkool-graphql:/data hhanh00/zkool-graphql:latest -d /data/zsa.db -l https://zsa.methyl.cc -p 8000
d44633b3a0d909a5632a63f708bc39b6d461ef1807cc39d2edb47438ec5eb98f

User: “what is the current height?”

$ docker logs zkool-zsa 2>&1 | tail -20
2026-10-04T01:46:46.770452Z  WARN zkool_graphql: Server is running WITHOUT authentication. Everyone has full access.
2026-10-04T01:46:46.770603Z  INFO zkool_graphql: Sapling parameters not found, downloading …
2026-10-04T01:46:46.771498Z  INFO rlz::api::sapling: Downloading Sapling parameter sapling-spend.params …

$ for i in $(seq 1 30); do curl ... done; docker logs zkool-zsa 2>&1 | tail -8
2026-10-04T01:46:50.506662Z  INFO rlz::api::sapling: Successfully downloaded and verified sapling-spend.params
2026-10-04T01:46:50.509020Z  INFO rlz::api::sapling: Downloading Sapling parameter sapling-output.params …
2026-10-04T01:46:51.336422Z  INFO rlz::api::sapling: Successfully downloaded and verified sapling-output.params
2026-10-04T01:46:51.336762Z  INFO zkool_graphql: Sapling parameters downloaded successfully
2026-10-04T01:46:51.336762Z  INFO zkool_graphql: db_path /data/zsa.db lwd_url https://zsa.methyl.cc port 8000 zebra false
2026-10-04T01:46:51.479719Z  INFO zkool_graphql: Listening on 0.0.0.0:8000
2026-10-04T01:46:53.210526Z  INFO rlz::graphql::frost: new_block 191804

$ curl -s http://localhost:8000/graphql -H 'Content-Type: application/json' \
    -d '{"query":"{ __schema { queryType { fields { name } } } }"}'
{"data":{"__schema":{"queryType":{"fields":[
  {"name":"apiVersion"},{"name":"accounts"},{"name":"transactionsByAccount"},
  {"name":"transactionById"},{"name":"memosByTransaction"},{"name":"balanceByAccount"},
  {"name":"addressByAccount"},{"name":"unconfirmedByAccount"},{"name":"notesByAccount"},
  {"name":"assets"},{"name":"currentHeight"},{"name":"prepareSend"},{"name":"decodePczt"},
  {"name":"signTx"},{"name":"broadcastTx"}]}}}}

$ curl -s http://localhost:8000/graphql -H 'Content-Type: application/json' -d '{"query":"{ currentHeight }"}'
{"data":{"currentHeight":191804}}

User: “create a new wallet and give me its address”

$ curl ... -d '{"query":"{ __schema { mutationType { fields { name args {...} } } } }"}'
mutationType fields (partial): createAccount(newAccount: NewAccount!),
  editAccount(idAccount: Int!, updateAccount: UpdateAccount!),
  deleteAccount(idAccount: Int!), resetAccount(idAccount: Int!),
  synchronize(idAccounts: ..., fast: Boolean, transparentLimit: Int), ...

$ curl ... -d '{"query":"{ __type(name: \"NewAccount\") { inputFields {...} } }"}'
NewAccount inputFields:
  name        String!  (NON_NULL)
  key         String!  (NON_NULL)
  passphrase  String
  aindex      Int!     (NON_NULL)
  birth       Int
  pools       Int
  useInternal Boolean! (NON_NULL)
  hw          Int

$ curl -s "https://api.github.com/repos/hhanh00/zkool" | python3 -c ...
main None                      (default branch = main)

$ curl -s "https://api.github.com/repos/hhanh00/zkool/git/trees/main?recursive=1" | (filter: seed/mnemonic/bip39/account/graphql)
lib/accounts.dart
lib/accounts.g.dart
lib/pages/accounts
lib/pages/accounts/manager.dart
lib/pages/accounts/multipay.dart
lib/pages/accounts/new_import.dart
lib/pages/accounts/pay_uri.dart
lib/pages/accounts/rescan.dart
lib/pages/accounts/send.dart
lib/pages/accounts/submit.dart
lib/pages/accounts/swap.dart
lib/pages/accounts/swap
lib/pages/accounts/swap/history.dart
lib/pages/accounts/txplan.dart

$ curl -s "https://raw.githubusercontent.com/hhanh00/zkool/main/lib/pages/accounts/new_import.dart" -o /tmp/new_import.dart
298 /tmp/new_import.dart

$ grep -E "key|seed|random|generate|NewAccount|phrase" /tmp/new_import.dart
line 25:   final SeedInfo? seedInfo;
line 38:   String _key = '';
line 54:       _key = si.seed;
line 128:  _key,
line 129:  name: 'key',
line 177:  final bool isNew = _key.isEmpty;
line 178:  if (isNew) _key = await warp.generateSeed();
line 184:  final account = await createNewAccount(
line 249: Future<int?> createNewAccount(
line 262:    await warp.createAccount(coin, name, key, index, birth, tOnly, isNew);

Here I interrupted because the AI was looking for ways to generate a seed phrase. It didn’t know that leaving the field empty would instruct the server to generate a random one.

User: “leave the key empty for a new account”

$ curl ... -d '{"query":"{ __type(name: \"Account\") { fields { name } } }"}'
{"data":null}        (then)
OBJECT types: Account, Addresses, AssetInfo, Balance, Event, MigrationEvent, Mutation, Note,
  Output, Query, Subscription, Transaction, UnconfirmedNote, UnconfirmedTx, UnsignedTx, ...

$ curl ... -d '{"query":"{ __type(name: \"Account\") { fields { name } } }"}'
Account fields: id, name, seed, passphrase, aindex, dindex, birth, height, balance, transactions

$ curl -s http://localhost:8000/graphql -H 'Content-Type: application/json' \
    -d '{"query":"mutation { createAccount(newAccount: {name: \"zsa-wallet\", key: \"\", aindex: 0, useInternal: false}) { id name } }"}'
{"errors":[
 {"message":"Field \"createAccount\" must not have a selection since type Int! has no subfields",...},
 {"message":"Unknown field \"id\" on type \"Int\"",...},
 {"message":"Unknown field \"name\" on type \"Int\"",...}]}

$ curl -s http://localhost:8000/graphql -H 'Content-Type: application/json' \
    -d '{"query":"mutation { createAccount(newAccount: {name: \"zsa-wallet\", key: \"\", aindex: 0, useInternal: false}) }"}'
{"data":{"createAccount":1}}

$ docker logs zkool-zsa 2>&1 | tail -3
2026-10-04T01:46:51.479719Z  INFO zkool_graphql: Listening on 0.0.0.0:8000
2026-10-04T01:46:53.210526Z  INFO rlz::graphql::frost: new_block 191804
2026-10-04T01:49:53.584435Z  INFO rlz::graphql::frost: new_block 191805

$ curl ... -d '{"query":"{ __type(name: \"Addresses\") { fields { name } } ... }"}'
Addresses fields: ua, transparent, sapling, orchard, ironwood, diversifierIndex
accounts args: [accountFilter]
addressByAccount args: [idAccount, pools]

$ curl -s http://localhost:8000/graphql -H 'Content-Type: application/json' \
    -d '{"query":"{ addressByAccount(idAccount: 1) { ua transparent } }"}'
{"data":{"addressByAccount":{
  "ua":"uregtest1a8r4j0maynkg57zvstxpk7x05sqv6uqszjpznfw76tyavppf86xha2fhnvz0jsfulnre3wzn9ce43tk8zhjn3hw8twedv2k3ekg2jhw65vce3lfwe6cvx64vdzxxrutvm9tpqenpwajr0e9upre8z3rud0jwf764mstmunqk5sm2a8af",
  "transparent":"tm9vs9QGkE1LmiHpUjHMSU5NGBSZYKQanCb"}}}

User: “monitor the balance every 15 seconds (need to sync and get balance)”

$ curl ... -d '{"query":"{ __type(name: \"Balance\") ... synchronize ... balanceByAccount ... }"}'
Balance fields: height, transparent, sapling, orchard, ironwood, total
synchronize args: idAccounts (NON_NULL LIST), fast: Boolean, transparentLimit: Int
balanceByAccount args: idAccount: Int!, height: Int

$ curl ... -d '{"query":"mutation { synchronize(idAccounts: [1]) }"}'
{"data":{"synchronize":191809}}
$ curl ... -d '{"query":"{ balanceByAccount(idAccount: 1) { height transparent sapling orchard total } }"}'
{"data":{"balanceByAccount":{"height":191809,"transparent":"0","sapling":"0","orchard":"0.01000000","total":"0.01000000"}}}

# wrote /var/folders/.../T/opencode/monitor_balance.sh:
#!/bin/zsh
URL=http://localhost:8000/graphql
while true; do
  sync=$(curl -s "$URL" -H 'Content-Type: application/json' -d '{"query":"mutation { synchronize(idAccounts: [1]) }"}')
  bal=$(curl -s "$URL" -H 'Content-Type: application/json' -d '{"query":"{ balanceByAccount(idAccount: 1) { ... } }"}')
  echo "$(date '+%H:%M:%S') sync: $sync bal: $bal" >> /tmp/opencode/zsa-balance.log
  sleep 15
done

$ chmod +x .../monitor_balance.sh && nohup .../monitor_balance.sh >/dev/null 2>&1 & echo "monitor PID: $!" && sleep 20 && tail -3 /tmp/opencode/zsa-balance.log
monitor PID: 1421
tail: /tmp/opencode/zsa-balance.log: No such file or directory

I went to the faucet site https://faucet.zsa.methyl.cc to request some test coins. The AI cannot do it for you because of the Captcha.

User: “stop the monitor, there are funds”

$ kill 1421 2>/dev/null; pkill -f monitor_balance.sh 2>/dev/null; sleep 1; pgrep -fl monitor_balance.sh || echo "monitor stopped"
monitor stopped

$ curl ... -d '{"query":"mutation { synchronize(idAccounts: [1]) }"}'
{"data":{"synchronize":191809}}
$ curl ... -d '{"query":"{ balanceByAccount(idAccount: 1) { height transparent sapling orchard total } }"}'
{"data":{"balanceByAccount":{"height":191809,"transparent":"0","sapling":"0","orchard":"0.01000000","total":"0.01000000"}}}

$ curl ... -d '{"query":"{ __type(name: \"UnconfirmedTx\") { fields { name } } }"}'
UnconfirmedTx fields: txid, value, notes
$ curl ... -d '{"query":"{ unconfirmedByAccount(idAccount: 1) { id amount } }"}'
{"errors":[{"message":"Unknown field \"id\" on type \"UnconfirmedTx\"",...},
           {"message":"Unknown field \"amount\" on type \"UnconfirmedTx\"",...}]}
$ curl ... -d '{"query":"{ unconfirmedByAccount(idAccount: 1) { txid value } }"}'
{"data":{"unconfirmedByAccount":[]}}

User: “issue new ZSA named ZKOOL, 1000000 units, finalized”

$ curl ... -d '{"query":"{ __schema { mutationType { fields { name } } } }"}'
['createAccount','editAccount','deleteAccount','resetAccount','synchronize','synchronizeAccount',
 'pay','issueAsset','setAssetName','newAddresses','dkgStart','dkgCancel','dkgSetAddress',
 'doDkg','frostSign','stepMigration','frostCancel']

$ curl ... -d '{"query":"{ __schema { mutationType { ... } } __type(name: \"AssetInfo\") {...} }"}'
issueAsset args: idAccount Int!, assetName String!, amount String!, firstIssuance Boolean!, finalize Boolean!
setAssetName args: idAsset Int!, name String!
AssetInfo fields: idAsset, assetDescHash, assetName, ik, assetBase, finalized, firstSeenHeight, balance

$ curl -s http://localhost:8000/graphql -H 'Content-Type: application/json' \
    -d '{"query":"mutation { issueAsset(idAccount: 1, assetName: \"ZKOOL\", amount: \"1000000\", firstIssuance: true, finalize: true) }"}'
{"data":{"issueAsset":"113c24662a515f988a74ef334b5d67cf6f1774ee6a2761c777c67a6e4a80e64c"}}

$ docker logs zkool-zsa 2>&1 | tail -5
2026-10-04T01:55:37.142185Z  INFO transaction: rlz::pay::plan: Extracting Tx
2026-10-04T01:55:37.147263Z  INFO rlz::pay::plan: Tx Extracted
2026-10-04T01:55:37.147275Z  INFO transaction: rlz::pay::plan: Tx Ready - 9744 bytes
2026-10-04T01:55:37.472476Z  INFO rlz::api::issuance: Issued asset "ZKOOL" (amount=1000000, first=true, finalize=true), tx size=9744 bytes
2026-10-04T01:55:39.001765Z  INFO transaction: rlz::pay: TXID: 113c24662a515f988a74ef334b5d67cf6f1774ee6a2761c777c67a6e4a80e64c

$ sleep 3; curl ... '{"query":"mutation { synchronize(idAccounts: [1]) }"}'
{"data":{"synchronize":191810}}
$ curl ... '{"query":"{ assets { assetName finalized balance { total } } }"}'
{"errors":[
 {"message":"Field \"assets\" argument \"idAccount\" of type \"Int!\" is required but not provided",...},
 {"message":"Field \"balance\" must not have a selection since type BigDecimal! has no subfields",...},
 {"message":"Unknown field \"total\" on type \"BigDecimal\"",...}]}

$ curl ... '{"query":"{ assets(idAccount: 1) { assetName finalized balance } }"}'
{"data":{"assets":[]}}

$ sleep 8; sync → {"data":{"synchronize":191811}}
$ assets → {"data":{"assets":[]}}
$ balance → {"data":{"balanceByAccount":{"total":"0.01000000"}}}
$ docker logs zkool-zsa 2>&1 | tail -4
[... Issued asset "ZKOOL" ..., TXID: 113c2466..., 2026-10-04T01:56:09.118802Z new_block 191811]

$ curl ... '{"query":"{ unconfirmedByAccount(idAccount: 1) { txid value } }"}'
{"data":{"unconfirmedByAccount":[{"txid":"d819faf43c37496abb012c34fc3a327a16cabfc3100544056c090940e94f38c4","value":"-0.01000000"}]}}
$ curl ... '{"query":"{ __type(name: \"Transaction\") { fields { name } } }"}'
Transaction fields: id, txid, account, height, time, value, fee, notes, outputs, spends

$ curl ... '{"query":"{ transactionById(idAccount: 1, txid: \"113c2466...\") { txid height value fee } }"}'
{"data":null,"errors":[{"message":"Unknown txid",...}]}
$ curl ... '{"query":"{ transactionById(idAccount: 1, txid: \"d819faf4...\") { txid height value fee } }"}'
{"data":null,"errors":[{"message":"Unknown txid",...}]}
$ curl ... '{"query":"mutation { synchronize(idAccounts: [1]) }"}'
{"data":{"synchronize":191811}}

$ for i in $(seq 1 10); do sync; assets; if changed → break; sleep 15; done
09:57:01 height=191812 assets={"data":{"assets":[{"assetName":"ZKOOL","finalized":true,"balance":"1000000"}]}}

User: “show my balance”

$ curl ... '{"query":"mutation { synchronize(idAccounts: [1]) }"}'
{"data":{"synchronize":191813}}
$ curl ... '{"query":"{ balanceByAccount(idAccount: 1) { height transparent sapling orchard total } }"}'
{"data":{"balanceByAccount":{"height":191813,"transparent":"0","sapling":"0","orchard":"0.00980000","total":"0.00980000"}}}
$ curl ... '{"query":"{ assets(idAccount: 1) { assetName finalized balance } }"}'
{"data":{"assets":[{"assetName":"ZKOOL","finalized":true,"balance":"1000000"}]}}

User: “create a new account”

$ curl -s http://localhost:8000/graphql -H 'Content-Type: application/json' \
    -d '{"query":"mutation { createAccount(newAccount: {name: \"account-2\", key: \"\", aindex: 1, useInternal: false}) }"}'
{"data":{"createAccount":2}}

$ curl ... '{"query":"{ addressByAccount(idAccount: 2) { ua transparent } accounts { id name } }"}'
{"data":{"addressByAccount":{
  "ua":"uregtest1lx5hmkw84klz9678hvwk5c7dq38pk2yrt0f7vkvzneg63w3ul8fq8p46ylfx67t228xrtg2n0clwj2gwjlz6thatvedpegd8d6lmeewhguswdhg2c2evdr57gh4w6et5pykec5xqkag9eknzvzm5putw6jqcp0yxvaeudeepdgrly7h9",
  "transparent":"tmYu5AyG7mLGRftEmtBmprfPZrHHkfQxCe8"},
  "accounts":[{"id":1,"name":"zsa-wallet"},{"id":2,"name":"account-2"}]}}

User: “send 500 ZKOOL from account 1 to 2”

$ curl ... -d '{"query":"{ __schema { mutationType { ... pay ... } } __type(name: \"Payment\") {...} }"}'
pay args: idAccount Int!, payment Payment!
Payment inputFields: recipients (NON_NULL LIST), srcPools Int, recipientPaysFee Boolean, confirmations Int

$ curl ... -d '{"query":"{ __type(name: \"Recipient\") { inputFields {...} } }"}'
Recipient inputFields: address String!, amount BigDecimal!, memo String, assetDesc String

$ python3 - <<'EOF'
import json, urllib.request
ua = "uregtest1lx5hmkw84klz9678hvwk5c7dq38pk2yrt0f7vkvzneg63w3ul8fq8p46ylfx67t228xrtg2n0clwj2gwjlz6thatvedpegd8d6lmeewhguswdhg2c2evdr57gh4w6et5pykec5xqkag9eknzvzm5putw6jqcp0yxvaeudeepdgrly7h9"
q = 'mutation { pay(idAccount: 1, payment: {recipients: [{address: "%s", amount: "500", assetDesc: "ZKOOL"}]}) }' % ua
req = urllib.request.Request("http://localhost:8000/graphql", data=json.dumps({"query": q}).encode(), headers={"Content-Type": "application/json"})
print(urllib.request.urlopen(req, timeout=120).read().decode())
EOF
{"data":{"pay":"219f8c0d74d0f1ea8f67c1ee17ae8c3919645a0f677cf0a765c429b20689cf65"}}

$ docker logs zkool-zsa 2>&1 | tail -4
2026-10-04T01:59:07.402566Z  INFO transaction: rlz::pay::plan: Extracting Tx
2026-10-04T01:59:07.405618Z  INFO transaction: rlz::pay::plan: Tx Ready - 12569 bytes
2026-10-04T01:59:07.884405Z  INFO transaction: rlz::pay: TXID: 219f8c0d74d0f1ea8f67c1ee17ae8c3919645a0f677cf0a765c429b20689cf65

$ for i in $(seq 1 10); do sync; assets(1); assets(2); if ZKOOL in acct2 → break; sleep 15; done
09:59:19 height=191815 acct1={"data":{"assets":[{"assetName":"ZKOOL","balance":"1000000"}]}} acct2={"data":{"assets":[]}}
09:59:35 height=191815 acct1={"data":{"assets":[{"assetName":"ZKOOL","balance":"1000000"}]}} acct2={"data":{"assets":[]}}
09:59:54 height=191816 acct1={"data":{"assets":[{"assetName":"ZKOOL","balance":"999500"}]}} acct2={"data":{"assets":[{"assetName":"ZKOOL","balance":"500"}]}}

User: “write a transcript of this session to a file”

$ write /Users/hanh/zkool-graphql/session-transcript.md
File created successfully.

Final state

  • Container zkool-zsa (id d44633b3a0d9) running, port 8000, db ~/zkool-graphql/zsa.db
  • Height 191816; accounts: 1 zsa-wallet (999,500 ZKOOL, 0.0098 ZEC orchard), 2 account-2 (500 ZKOOL)
  • Issuance TXID 113c24662a515f988a74ef334b5d67cf6f1774ee6a2761c777c67a6e4a80e64c (finalized)
  • Transfer TXID 219f8c0d74d0f1ea8f67c1ee17ae8c3919645a0f677cf0a765c429b20689cf65
3 Likes

Hey @hanh, this is excellent. Thanks for posting the full AI + Docker transcript; that kind of “here’s exactly what worked” write-up is how more people actually get onto the ZSA testnet. If it’s useful, we’re happy to compare notes on the sharp edges we hit on this net (issuance witnesses / compact scan, V6 + zip-233, ZIP-317 fees). No rush, just glad another wallet team is actively exercising OrchardZSA in the open.

Appreciate the work you’re doing with ZKOOL. Looking forward to more shared testnet progress.

1 Like

There has been some confusion regarding this testnet.

To be clear, the work on the ZSA testnet at https://zsa.methyl.cc is my own but it is based on the qedit code. However, for better or for worse, that was code isn’t merged upstream. By now, with the arrival of Ironwood and the upcoming NU7, etc it has diverged significantly.

I took upon myself to:

  • fork out zebra, lightwalletd, orchard, and any other dependent library that makes an end to end environment for ZSA
  • merged Qedit repos into a recent zebra so that we pick the security fixes (including the Orchard circuit fix). They are dated, so the merge wasn’t easy
  • integrated ZSA, which assumes a different protocol path than Ironwood. Both changed the note format! ZSA introduces asset base and Ironwood quantum resistance. Now we have incompatible Note V3, which is fine as long as the server are different. The client is another story. ZKOOL connects to both types of network and therefore need to speak both protocols. There is a lot of incompatiblity there
  • Add support for ZSA into ZKOOL (GUI and CLI)
  • Finally setup a persistent testnet and a faucet app.

Again, this is not affiliated with Qedit or ZCG. They both have their own efforts.

The goal is to gauge the amount of work required to move ZSA forward, and offer a fully operational platform for developers to build ZSA apps on.

9 Likes

Do you/or anyone have a summary a summary anywhere of the actual functionality of ZSAs? I’m very interested in them on zcash but there is very little info out there in a concise format that details how they would work at a high level.

It’s all documented in ZIPs, so an AI search will find everything there is.

I’m writing one for like a day, sort of a beginner’s guide, I’ll link you once I’m done in a few hours.

1 Like

I forgot to mention that ZKOOL has been supporting ZSA testnet for a while.

Here’s a tutorial on how to create a ZSA wallet.

Create a ZSA wallet (database)

  1. Toggle the database manager on restart:

This requires a relaunch of the app.

  1. Create a zsa database

The name must contain zsa

Select and open the zsa database.

Select the ZSA lightwalletd

At this point, the wallet is still connected to its default LWD and shows a height of ~3m.

  1. In the settings, set the lwd to https://zsa.methyl.cc

  1. Tap on the height to refresh it. It should be ~100-200 k

The testnet has been running for a few months.

Create accounts

  1. Create / Import accounts as usual.
  2. Get test coins: there is a faucet at https://faucet.zsa.methyl.cc
  3. See your zsa holdings in the tab

Name the ZSA

When you restore an account, the wallet does not know the names of the ZSA since they are not part of the protocol. It will display their hash instead. You can set a name by long clicking on the hash.

The history will get updated like this

When you issue your own ZSA, the name is recorded automatically, but the recipients will have to set it (once).

Issuing new ZSA

Tap on the +

Sending

Replace ZEC with the ZSA you want to send (it’s a dropdown).

Receiving

You receive ZSA automatically at your normal Orchard address.

3 Likes