From c61d9f5cb72cf6134d7cc5149a3929fdac587d14 Mon Sep 17 00:00:00 2001 From: Michael Buntarman Date: Tue, 23 Jun 2026 17:22:26 +0700 Subject: [PATCH 1/2] feat: read any wallet's order-book positions and collateral --- bindings/bindings.go | 68 ++++++++++++++++++++ examples/maa_lifecycle_example/.env.example | 35 ++++++++++ examples/maa_lifecycle_example/README.md | 29 +++++---- examples/maa_lifecycle_example/main.py | 64 +++++++++++++++---- go.mod | 2 +- go.sum | 4 +- src/trufnetwork_sdk_py/client.py | 32 ++++++++++ tests/test_portfolio_by_wallet.py | 71 +++++++++++++++++++++ 8 files changed, 279 insertions(+), 26 deletions(-) create mode 100644 examples/maa_lifecycle_example/.env.example create mode 100644 tests/test_portfolio_by_wallet.py diff --git a/bindings/bindings.go b/bindings/bindings.go index 94d995c..fe64e36 100644 --- a/bindings/bindings.go +++ b/bindings/bindings.go @@ -2326,6 +2326,74 @@ func GetUserPositions(client *tnclient.Client) (string, error) { return string(jsonBytes), nil } +// GetPositionsByWallet returns a wallet's portfolio (holdings + open orders) by address. +// Unlike GetUserPositions (which reads @caller), this reads the wallet passed in. +func GetPositionsByWallet(client *tnclient.Client, walletAddress string) (string, error) { + ctx := context.Background() + + orderBook, err := client.LoadOrderBook() + if err != nil { + return "", errors.Wrap(err, "failed to load order book") + } + + results, err := orderBook.GetPositionsByWallet(ctx, types.GetPositionsByWalletInput{ + WalletHex: walletAddress, + }) + if err != nil { + return "", errors.Wrap(err, "failed to get positions by wallet") + } + + positions := make([]map[string]any, len(results)) + for i, pos := range results { + positions[i] = map[string]any{ + "query_id": pos.QueryID, + "outcome": pos.Outcome, + "price": pos.Price, + "amount": pos.Amount, + "position_type": pos.PositionType, + } + } + + jsonBytes, err := json.Marshal(positions) + if err != nil { + return "", errors.Wrap(err, "failed to marshal positions") + } + + return string(jsonBytes), nil +} + +// GetCollateralByWallet returns a wallet's total locked collateral by address on a bridge. +// Unlike GetUserCollateral (which reads @caller), this reads the wallet passed in. +func GetCollateralByWallet(client *tnclient.Client, walletAddress string, bridge string) (string, error) { + ctx := context.Background() + + orderBook, err := client.LoadOrderBook() + if err != nil { + return "", errors.Wrap(err, "failed to load order book") + } + + result, err := orderBook.GetCollateralByWallet(ctx, types.GetCollateralByWalletInput{ + WalletHex: walletAddress, + Bridge: bridge, + }) + if err != nil { + return "", errors.Wrap(err, "failed to get collateral by wallet") + } + + collateral := map[string]any{ + "total_locked": result.TotalLocked, + "buy_orders_locked": result.BuyOrdersLocked, + "shares_value": result.SharesValue, + } + + jsonBytes, err := json.Marshal(collateral) + if err != nil { + return "", errors.Wrap(err, "failed to marshal collateral") + } + + return string(jsonBytes), nil +} + // GetMarketDepth returns aggregated volume per price level func GetMarketDepth(client *tnclient.Client, queryID int, outcome bool) (string, error) { ctx := context.Background() diff --git a/examples/maa_lifecycle_example/.env.example b/examples/maa_lifecycle_example/.env.example new file mode 100644 index 0000000..7d0d8d3 --- /dev/null +++ b/examples/maa_lifecycle_example/.env.example @@ -0,0 +1,35 @@ +# MAA lifecycle smoke test — configuration. +# +# Copy this file to `.env` (same directory) and fill in the two private keys: +# +# cp .env.example .env +# +# `.env` is gitignored. Real environment variables take precedence over `.env`, +# so you can still override any value with a shell export. + +# --- required: two DISTINCT keys ------------------------------------------------- +# Both accept the key with OR without a 0x prefix. +# AGENT = the restricted agent (signs maa_create_rule, runs allow-listed actions AS the MAA). +# OWNER = the unrestricted owner/funder (joins, funds, withdraws; must hold bridged TRUF). +AGENT_PRIVATE_KEY= +OWNER_PRIVATE_KEY= + +# --- network ------------------------------------------------------------------- +# The testnet RPC/gateway endpoint. +PROVIDER_URL=https://gateway.testnet.truf.network + +# --- token / funding ----------------------------------------------------------- +# Bridge namespace used to FUND the MAA and pay write fees (the universal fee bridge on dev/testnet). +MAA_BRIDGE=hoodi_tt +# Amount (wei, NUMERIC(78,0)) to fund the MAA with. Must cover the action fees +# (create_streams may cost 100 TRUF where the fee is active). +MAA_FUND_AMOUNT=250000000000000000000 +# Owner-withdraw commission paid to the agent, in basis points (250 = 2.5%). +MAA_FEE_BPS=250 + +# --- portfolio read (migration 051) -------------------------------------------- +# Order-book collateral bridge used by get_collateral_by_wallet. This is the bridge +# the order-book MARKETS settle in (hoodi_tt2 / sepolia_bridge / ethereum_bridge on +# dev/testnet) — NOT the hoodi_tt funding/fee bridge above. get_positions_by_wallet +# needs no bridge. +MAA_COLLATERAL_BRIDGE=hoodi_tt2 diff --git a/examples/maa_lifecycle_example/README.md b/examples/maa_lifecycle_example/README.md index 139f997..5ab3cda 100644 --- a/examples/maa_lifecycle_example/README.md +++ b/examples/maa_lifecycle_example/README.md @@ -21,6 +21,11 @@ make that safe, mirroring the node's canonical oracle `tests/streams/maa/data_ag 5. **Local derivation matches the chain** — `rule_id` and the MAA address are derived offline (`compute_rules_hash` / `derive_rule_id` / `derive_maa_address_hex`) and asserted equal to what the node returns. +6. **Portfolio reads by address** — `get_positions_by_wallet` / `get_collateral_by_wallet` + (migration 051) read the agent wallet's order-book inventory *by address* — the signer is not the + wallet being read — so an owner or a delegated market-maker bot can monitor an agent wallet it + does not sign for. This MAA does data provision (not order-book trading), so the reads return + empty/zero; a clean return is the proof that 051 is live. ## Two identities (required) @@ -33,32 +38,34 @@ The agent and the owner are **different keys**: ## Run +Configuration is read from a `.env` file next to the script (real environment variables still +take precedence, so you can override any value with a shell `export`). `.env` is gitignored. + ```bash # from the repo root uv venv .venv && source .venv/bin/activate && uv pip install -e ".[dev]" cd examples/maa_lifecycle_example - -export PROVIDER_URL="https://gateway.testnet.truf.network" # the testnet RPC/gateway -export AGENT_PRIVATE_KEY=0x... # restricted agent -export OWNER_PRIVATE_KEY=0x... # unrestricted owner (must hold bridged TRUF) -export MAA_BRIDGE="hoodi_tt" # token bridge namespace on this network -export MAA_FUND_AMOUNT="250000000000000000000" # wei to fund the MAA with -# optional: export MAA_FEE_BPS=250 # owner-withdraw commission to the agent (2.5%) +cp .env.example .env # then edit .env: fill in AGENT_PRIVATE_KEY and OWNER_PRIVATE_KEY python main.py ``` -### Environment variables +Each run uses a fresh salt by default, so it registers a new rule/MAA and can be re-run without +colliding with a previous run. Set `MAA_SALT` (64 hex chars) to pin a reproducible rule_id. + +### Environment variables (see `.env.example`) | Variable | Default | Notes | |----------|---------|-------| | `PROVIDER_URL` | `https://gateway.testnet.truf.network` | The testnet RPC/gateway. **Confirm the real URL for your network.** | -| `AGENT_PRIVATE_KEY` | — (required) | Restricted agent key. | -| `OWNER_PRIVATE_KEY` | — (required) | Unrestricted owner key; must hold ≥ `MAA_FUND_AMOUNT` + fees of bridged token. | -| `MAA_BRIDGE` | `hoodi_tt` | Token bridge namespace. dev = `hoodi_tt`/`hoodi_tt2`, mainnet = `eth_truf`/`eth_usdc`. **Confirm which exists on testnet.** | +| `AGENT_PRIVATE_KEY` | — (required) | Restricted agent key (with or without `0x`). | +| `OWNER_PRIVATE_KEY` | — (required) | Unrestricted owner key (with or without `0x`); must hold ≥ `MAA_FUND_AMOUNT` + fees of bridged token. | +| `MAA_BRIDGE` | `hoodi_tt` | Funding/fee bridge namespace. dev = `hoodi_tt`/`hoodi_tt2`, mainnet = `eth_truf`/`eth_usdc`. | | `MAA_FUND_AMOUNT` | `250000000000000000000` (250 TRUF) | Must cover the action fees (`create_streams` may cost 100 TRUF where the fee is active). | | `MAA_FEE_BPS` | `250` (2.5%) | Owner-withdraw commission paid to the agent. | +| `MAA_COLLATERAL_BRIDGE` | `hoodi_tt2` | Order-book bridge for `get_collateral_by_wallet` (migration 051) — the bridge the markets settle in, **not** the `hoodi_tt` fee bridge. | +| `MAA_SALT` | _(fresh per run)_ | Optional 64-hex salt to pin a reproducible rule_id across runs. | ## Open items to confirm before the first run diff --git a/examples/maa_lifecycle_example/main.py b/examples/maa_lifecycle_example/main.py index de8f248..b8432d4 100644 --- a/examples/maa_lifecycle_example/main.py +++ b/examples/maa_lifecycle_example/main.py @@ -15,17 +15,13 @@ This mirrors the node's canonical oracle, tests/streams/maa/data_agent_test.go. -Two keys are required (the agent and the owner are DIFFERENT identities): - - export PROVIDER_URL="https://gateway.testnet.truf.network" # the testnet RPC/gateway - export AGENT_PRIVATE_KEY=0x... # restricted: signs maa_create_rule + runs actions as the MAA - export OWNER_PRIVATE_KEY=0x... # unrestricted: joins, funds, withdraws (must hold bridged TRUF) - export MAA_BRIDGE="hoodi_tt" # the token bridge namespace on this network (confirm per env) - export MAA_FUND_AMOUNT="250000000000000000000" # wei to fund the MAA with (NUMERIC(78,0)) +Config comes from a .env file next to this script (real environment variables still take +precedence). Two DISTINCT keys are required — the agent and the owner are different identities: + cp .env.example .env # then fill in AGENT_PRIVATE_KEY and OWNER_PRIVATE_KEY python main.py -See README.md for what success looks like and the open items to confirm before the first run. +See .env.example for every setting, and README.md for what success looks like. """ import os @@ -40,13 +36,34 @@ derive_maa_address_hex, ) -# --- configuration (all overridable via environment) ----------------------------------------------- + +# --- load .env (zero-dependency; real environment variables take precedence) ------------------------ +def _load_dotenv(path: str) -> None: + """Populate os.environ from a KEY=VALUE .env file, without overriding existing vars.""" + if not os.path.exists(path): + return + with open(path) as fh: + for raw in fh: + line = raw.strip() + if not line or line.startswith("#") or "=" not in line: + continue + key, _, val = line.partition("=") + os.environ.setdefault(key.strip(), val.strip().strip('"').strip("'")) + + +_load_dotenv(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".env")) + +# --- configuration (all overridable via environment / .env; see .env.example) ---------------------- PROVIDER_URL = os.getenv("PROVIDER_URL", "https://gateway.testnet.truf.network") AGENT_PRIVATE_KEY = os.getenv("AGENT_PRIVATE_KEY") # restricted agent OWNER_PRIVATE_KEY = os.getenv("OWNER_PRIVATE_KEY") # unrestricted owner / funder -BRIDGE = os.getenv("MAA_BRIDGE", "hoodi_tt") # token bridge namespace (e.g. hoodi_tt / eth_truf) +BRIDGE = os.getenv("MAA_BRIDGE", "hoodi_tt") # funding/fee bridge namespace (e.g. hoodi_tt / eth_truf) FUND_AMOUNT = os.getenv("MAA_FUND_AMOUNT", "250000000000000000000") # wei (NUMERIC(78,0)) FEE_BPS = int(os.getenv("MAA_FEE_BPS", "250")) # 2.5% owner-withdraw commission to the agent +# Order-book collateral bridge for get_collateral_by_wallet (migration 051). This is the bridge the +# order-book MARKETS settle in (hoodi_tt2 / sepolia_bridge / ethereum_bridge on dev/testnet), NOT the +# hoodi_tt funding/fee bridge above. get_positions_by_wallet needs no bridge. +COLLATERAL_BRIDGE = os.getenv("MAA_COLLATERAL_BRIDGE", "hoodi_tt2") # On-chain decimal types the inner actions declare. A NUMERIC argument MUST be wrapped in MAANumericArg # with these EXACT precision/scale, because JSON has no decimal type and the node does not coerce text @@ -54,8 +71,16 @@ TOKEN_PRECISION, TOKEN_SCALE = 78, 0 # bridge amounts: NUMERIC(78,0) VALUE_PRECISION, VALUE_SCALE = 36, 18 # primitive record values: NUMERIC(36,18) -# A fixed salt makes the rule_id (and therefore the MAA) deterministic for this agent+owner pair. -SALT = b"\x4d\x41\x41" + b"\x00" * 29 # b"MAA" + padding, 32 bytes +# A fresh salt per run keeps each smoke test independent: it yields a new rule_id (and therefore a +# new MAA), so re-running never collides with an already-registered rule. The local-derivation +# cross-checks below use this exact salt, so they stay valid. Set MAA_SALT (64 hex chars, optional +# 0x) to pin a reproducible rule_id across runs. +_salt_env = os.getenv("MAA_SALT") +if _salt_env: + SALT = bytes.fromhex(_salt_env[2:] if _salt_env.startswith("0x") else _salt_env) +else: + SALT = b"MAA" + int(time.time()).to_bytes(8, "big") + b"\x00" * 21 # 3 + 8 + 21 = 32 bytes +assert len(SALT) == 32, f"salt must be 32 bytes, got {len(SALT)}" NAMESPACES = ["main", "main"] ACTIONS = ["create_streams", "insert_records"] # the agent's allow-list (mirrors data_agent_test.go) @@ -212,12 +237,27 @@ def main() -> int: print(f" - {ev['event_type']:<12} role={ev['actor_role']:<13} actor={ev['actor_addr']} " f"action={ev.get('inner_action') or '-'} amount={ev.get('amount') or '-'}") + # (h) READ the agent wallet's ORDER-BOOK portfolio BY ADDRESS (migration 051). + # get_positions_by_wallet / get_collateral_by_wallet read the wallet you pass in (NOT + # @caller), so an owner — or a delegated market-maker bot — can read an agent wallet's live + # inventory without holding its key. The signer here (agent) differs from the wallet read + # (the MAA), which is the whole point. This MAA's allow-list is create_streams/insert_records + # (data provision), so it holds NO order-book positions — the reads return empty/zero. A clean + # return (instead of "unknown action") is the proof that migration 051 is live on this network. + banner("(h) read the agent wallet's order-book portfolio by address") + positions = agent.get_positions_by_wallet(maa) + collateral = agent.get_collateral_by_wallet(maa, COLLATERAL_BRIDGE) + print(f"get_positions_by_wallet({maa}) -> {len(positions)} positions: {positions}") + print(f"get_collateral_by_wallet({maa}, {COLLATERAL_BRIDGE}) -> {collateral}") + print("✅ address-parameterized portfolio reads are live (migration 051)") + banner("✅ MAA lifecycle smoke test PASSED") print("Proven on-chain via the Python SDK:") print(" • @caller rewritten to the MAA (the stream is owned by the wallet, not the agent key)") print(" • fees debit the MAA's own escrow") print(" • the restricted agent cannot move funds out") print(" • the owner withdraws with the agreed commission") + print(" • an owner can read the wallet's order-book positions/collateral by address (051)") return 0 diff --git a/go.mod b/go.mod index 76541d5..eafd499 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 github.com/pkg/errors v0.9.1 github.com/trufnetwork/kwil-db/core v0.4.3-0.20260615121733-0d71bd259558 - github.com/trufnetwork/sdk-go v0.7.3-0.20260622120135-f266b733466c + github.com/trufnetwork/sdk-go v0.7.3-0.20260623101902-a88c54ac6abb google.golang.org/genproto v0.0.0-20251111163417-95abcf5c77ba ) diff --git a/go.sum b/go.sum index 783c63a..d4dea9a 100644 --- a/go.sum +++ b/go.sum @@ -60,8 +60,8 @@ github.com/trufnetwork/kwil-db v0.10.3-0.20260615121733-0d71bd259558 h1:I49YGUiU github.com/trufnetwork/kwil-db v0.10.3-0.20260615121733-0d71bd259558/go.mod h1:LiBAC48uZl2B0IiLtD2hpOce7RNfpuDdghVAOc3u1Qo= github.com/trufnetwork/kwil-db/core v0.4.3-0.20260615121733-0d71bd259558 h1:m7a9HITFMXJF22QznIKoAdeiH8eZxWPU8IRzgcyNMo8= github.com/trufnetwork/kwil-db/core v0.4.3-0.20260615121733-0d71bd259558/go.mod h1:HnOsh9+BN13LJCjiH0+XKaJzyjWKf+H9AofFFp90KwQ= -github.com/trufnetwork/sdk-go v0.7.3-0.20260622120135-f266b733466c h1:CfoiZyqRH8VCLSQPrm77ntxgQgOfi/b99nGIukQR2eI= -github.com/trufnetwork/sdk-go v0.7.3-0.20260622120135-f266b733466c/go.mod h1:CivlBpc5RcYwyo8EysoAZWXXfmTtigfkR5VctivwSHc= +github.com/trufnetwork/sdk-go v0.7.3-0.20260623101902-a88c54ac6abb h1:NyKYWkkmeShfSR+2FjAg78s+k0LY5A95Yayh4H+yPjo= +github.com/trufnetwork/sdk-go v0.7.3-0.20260623101902-a88c54ac6abb/go.mod h1:CivlBpc5RcYwyo8EysoAZWXXfmTtigfkR5VctivwSHc= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= diff --git a/src/trufnetwork_sdk_py/client.py b/src/trufnetwork_sdk_py/client.py index e87f09a..88284f5 100644 --- a/src/trufnetwork_sdk_py/client.py +++ b/src/trufnetwork_sdk_py/client.py @@ -2804,6 +2804,38 @@ def get_user_collateral(self) -> UserCollateral: return cast(UserCollateral, json.loads(json_str)) + def get_positions_by_wallet(self, wallet_address: str) -> list[UserPosition]: + """Get a wallet's portfolio (holdings + open orders) by address. + + Unlike get_user_positions (which reads the signer), this reads the wallet you + pass in — so an owner can read an agent wallet's (MAA) positions without holding + its key. ``wallet_address`` is a 0x-prefixed (or bare) 20-byte hex address. + """ + json_str = truf_sdk.GetPositionsByWallet(self.client, wallet_address) + + if not json_str: + return [] + + return cast(list[UserPosition], json.loads(json_str)) + + def get_collateral_by_wallet(self, wallet_address: str, bridge: str) -> UserCollateral: + """Get a wallet's total locked collateral by address, on a given bridge. + + Unlike get_user_collateral (which reads the signer), this reads the wallet you + pass in. ``bridge`` is required (per-bridge token decimals), e.g. "hoodi_tt" / + "eth_truf". ``wallet_address`` is a 0x-prefixed (or bare) 20-byte hex address. + """ + json_str = truf_sdk.GetCollateralByWallet(self.client, wallet_address, bridge) + + if not json_str: + return { + "total_locked": "0", + "buy_orders_locked": "0", + "shares_value": "0", + } + + return cast(UserCollateral, json.loads(json_str)) + # ═══════════════════════════════════════════════════════════════ # ORDER BOOK - SETTLEMENT & REWARDS # ═══════════════════════════════════════════════════════════════ diff --git a/tests/test_portfolio_by_wallet.py b/tests/test_portfolio_by_wallet.py new file mode 100644 index 0000000..6515515 --- /dev/null +++ b/tests/test_portfolio_by_wallet.py @@ -0,0 +1,71 @@ +"""Pure unit tests for the address-parameterized portfolio getters. + +These exercise the Python-side wrapper logic of TNClient.get_positions_by_wallet / +get_collateral_by_wallet (argument forwarding, JSON parsing, empty handling) by +monkeypatching the Go binding, so they need no node. +""" + +import json + +import trufnetwork_sdk_py.client as client_mod +from trufnetwork_sdk_py.client import TNClient + +WALLET = "0x12aae9a9cf034cb71cbf17cfa1e9612cda8e8a87" + + +def _client_without_connect() -> TNClient: + # Bypass __init__ (which would open a network client); the wrappers only + # forward self.client to the (monkeypatched) binding. + c = TNClient.__new__(TNClient) + c.client = object() + return c + + +def test_get_positions_by_wallet_forwards_and_parses(monkeypatch): + captured = {} + + def fake_get_positions(client, wallet): + captured["wallet"] = wallet + return json.dumps( + [{"query_id": 7, "outcome": True, "price": -55, "amount": 100, "position_type": "buy_order"}] + ) + + monkeypatch.setattr(client_mod.truf_sdk, "GetPositionsByWallet", fake_get_positions, raising=False) + + out = _client_without_connect().get_positions_by_wallet(WALLET) + + assert captured["wallet"] == WALLET, "the wallet must be forwarded verbatim to the binding" + assert len(out) == 1 + assert out[0]["position_type"] == "buy_order" + assert out[0]["query_id"] == 7 + + +def test_get_positions_by_wallet_empty_returns_list(monkeypatch): + monkeypatch.setattr(client_mod.truf_sdk, "GetPositionsByWallet", lambda client, wallet: "", raising=False) + assert _client_without_connect().get_positions_by_wallet(WALLET) == [] + + +def test_get_collateral_by_wallet_forwards_and_parses(monkeypatch): + captured = {} + + def fake_get_collateral(client, wallet, bridge): + captured["wallet"] = wallet + captured["bridge"] = bridge + return json.dumps( + {"total_locked": "55000000000000000000", "buy_orders_locked": "55000000000000000000", "shares_value": "0"} + ) + + monkeypatch.setattr(client_mod.truf_sdk, "GetCollateralByWallet", fake_get_collateral, raising=False) + + out = _client_without_connect().get_collateral_by_wallet(WALLET, "hoodi_tt") + + assert captured["wallet"] == WALLET + assert captured["bridge"] == "hoodi_tt", "the bridge must be forwarded to the binding" + assert out["buy_orders_locked"] == "55000000000000000000" + assert out["total_locked"] == "55000000000000000000" + + +def test_get_collateral_by_wallet_empty_returns_zeros(monkeypatch): + monkeypatch.setattr(client_mod.truf_sdk, "GetCollateralByWallet", lambda client, wallet, bridge: "", raising=False) + out = _client_without_connect().get_collateral_by_wallet(WALLET, "hoodi_tt") + assert out == {"total_locked": "0", "buy_orders_locked": "0", "shares_value": "0"} From e88d669ea556c6f15c6f1f8bd4c2b66bfc34c25e Mon Sep 17 00:00:00 2001 From: Michael Buntarman Date: Tue, 23 Jun 2026 17:57:42 +0700 Subject: [PATCH 2/2] chore: keep the example salt unique within the same second --- examples/maa_lifecycle_example/main.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/maa_lifecycle_example/main.py b/examples/maa_lifecycle_example/main.py index b8432d4..8143a87 100644 --- a/examples/maa_lifecycle_example/main.py +++ b/examples/maa_lifecycle_example/main.py @@ -79,7 +79,9 @@ def _load_dotenv(path: str) -> None: if _salt_env: SALT = bytes.fromhex(_salt_env[2:] if _salt_env.startswith("0x") else _salt_env) else: - SALT = b"MAA" + int(time.time()).to_bytes(8, "big") + b"\x00" * 21 # 3 + 8 + 21 = 32 bytes + # Nanosecond precision so two runs in the same second still get distinct salts; the ns + # value (~1.8e18) fits in 8 unsigned bytes (max ~1.8e19). + SALT = b"MAA" + time.time_ns().to_bytes(8, "big") + b"\x00" * 21 # 3 + 8 + 21 = 32 bytes assert len(SALT) == 32, f"salt must be 32 bytes, got {len(SALT)}" NAMESPACES = ["main", "main"]