All skills

Guide for writing Python code with AlgoKit Utils (`algokit-utils`). Use this skill whenever the user is building on Algorand with Python — client setup, account management, payments, ASA operations, atomic transaction groups, smart contract deployment and interaction (AppFactory, AppClient, ARC-56/ARC-32 specs), raw app calls, TEAL compilation, key registration, network management, error handling, and the low-level crypto primitives in `algokit_crypto` (Ed25519 keygen/signing/verification, Peikert xHD BIP44 wallets, wrapped-secret patterns) and `algokit_common` (`sha512_256`). Trigger on imports from `algokit_utils` or `algokit_crypto`, references to `AlgorandClient`, `AppFactory`, `AppClient`, `AlgoAmount`, `ed25519_generator`, `peikert_hd_wallet_generator`, `sha512_256`, `WrappedEd25519Seed`, or `RawEd25519Signer`, or any Python code that builds on Algorand.

Use this Skill: https://skilld.dev/gh/algorand-devrel/algorand-agent-skills/algokit-utils-py

This session only. Nothing lands on disk.

referenceswrapped-secrets.md

≈2k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Wrapped secrets

All snippets below import from algokit_crypto. The wrapped-secret pattern keeps Ed25519 seeds and HD extended private keys out of long-lived memory: a secret is unwrapped for a single operation, consumed, then immediately re-wrapped and zeroed. Use it when you are integrating with an HSM, KMS, or any store that re-encrypts key material between uses.

Implement a WrappedEd25519Seed

Provide an unwrap/re-wrap pair for a 32-byte Ed25519 seed so the seed spends as little time unwrapped in memory as possible.

from algokit_crypto import WrappedEd25519Seed


class MyWrappedSeed:
    def __init__(self, store) -> None:
        self._store = store

    def unwrap_ed25519_seed(self) -> bytearray:
        # Return a 32-byte ``bytearray`` — for example decrypted from a secure store.
        # It must be a ``bytearray`` (not ``bytes``) so the consumer can zero it.
        return self._store.read()

    def wrap_ed25519_seed(self) -> None:
        # Re-lock / re-encrypt / discard the in-memory seed here.
        self._store.lock()


seed: WrappedEd25519Seed = MyWrappedSeed(store=...)

What just happened: WrappedEd25519Seed is a @runtime_checkable Protocol with two methods: unwrap_ed25519_seed returns exactly 32 plaintext bytes as a bytearray, and wrap_ed25519_seed is called afterwards to clean up (re-encrypt, flush buffers, touch hardware, etc.). The consumer — typically ed25519_signing_key_from_wrapped_secret — is responsible for invoking both and for zeroing the unwrapped buffer in-place via bytearray[:] = b"\x00" * len. Your implementation only has to move bytes in and out of your storage layer. Any object that structurally satisfies the protocol works — no base class required.

Implement a WrappedHdExtendedPrivateKey

Provide an unwrap/re-wrap pair for a 96-byte HD extended private key.

from algokit_crypto import WrappedHdExtendedPrivateKey


class MyWrappedExtendedKey:
    def __init__(self, store) -> None:
        self._store = store

    def unwrap_hd_extended_private_key(self) -> bytearray:
        # Return exactly 96 bytes: scalar (zL) || prefix (zR) || chain_code.
        return self._store.read()

    def wrap_hd_extended_private_key(self) -> None:
        self._store.lock()


key: WrappedHdExtendedPrivateKey = MyWrappedExtendedKey(store=...)

What just happened: WrappedHdExtendedPrivateKey carries a 96-byte value laid out as scalar (32) || prefix (32) || chain_code (32). Only the first 64 bytes are used for signing — if your store only keeps those 64 bytes, your unwrap function must pad the chain code back to 96 bytes before returning (typically with zeros, since the chain code is not needed for signing). Consumers enforce the 96-byte length and will raise ValueError if the buffer is the wrong size.

Create a signing key from a wrapped seed

Turn a WrappedEd25519Seed into an Ed25519SigningKey whose signer handles unwrap, sign, re-wrap, and zeroing automatically.

from algokit_crypto import (
    WrappedEd25519Seed,
    ed25519_signing_key_from_wrapped_secret,
)

wrapped_seed: WrappedEd25519Seed = ...

signing_key = ed25519_signing_key_from_wrapped_secret(wrapped_seed)
signature = signing_key["raw_ed25519_signer"](b"Hello, Algorand!")

What just happened: ed25519_signing_key_from_wrapped_secret inspected the wrapped object via isinstance(wrapped, WrappedEd25519Seed), saw unwrap_ed25519_seed/wrap_ed25519_seed, and unwrapped the seed once immediately to derive the public key. It then returned an Ed25519SigningKey TypedDict whose raw_ed25519_signer unwraps the seed again on every call, signs the bytes via PyNaCl's SigningKey, re-wraps the seed in a finally block, and finally fills the unwrapped bytearray with zeros — so plaintext seed material exists in memory only for the duration of a single sign call.

Create a signing key from a wrapped HD extended key

The same helper accepts a WrappedHdExtendedPrivateKey and dispatches to the HD signing path instead.

from algokit_crypto import (
    WrappedHdExtendedPrivateKey,
    ed25519_signing_key_from_wrapped_secret,
)

wrapped_extended_key: WrappedHdExtendedPrivateKey = ...

signing_key = ed25519_signing_key_from_wrapped_secret(wrapped_extended_key)
signature = signing_key["raw_ed25519_signer"](b"Hello, Algorand!")

What just happened: When the wrapped object exposes unwrap_hd_extended_private_key/wrap_hd_extended_private_key, the helper uses the package's internal _raw_sign (which consumes a 64-byte HD-expanded secret via xhd_wallet_api_py.public_key and nacl.bindings) rather than PyNaCl's high-level SigningKey.sign. The 96-byte buffer is validated for length on every unwrap and is zeroed after each use with the same try/finally discipline as the seed path.

Use the pinned PyNaCl variant explicitly

Import pynacl_ed25519_signing_key_from_wrapped_secret when you need to guarantee the current backend.

from algokit_crypto import (
    WrappedEd25519Seed,
    pynacl_ed25519_signing_key_from_wrapped_secret,
)

wrapped_seed: WrappedEd25519Seed = ...

signing_key = pynacl_ed25519_signing_key_from_wrapped_secret(wrapped_seed)

What just happened: ed25519_signing_key_from_wrapped_secret is an alias for pynacl_ed25519_signing_key_from_wrapped_secret. If you want to pin the PyNaCl (libsodium) implementation specifically — and be insulated from future default-repointing — import the pynacl_* name directly.

Handle unwrap and re-wrap failures

When the underlying operation and the re-wrap both fail, you receive an ExceptionGroup carrying both.

from exceptiongroup import ExceptionGroup

from algokit_crypto import (
    WrappedEd25519Seed,
    ed25519_signing_key_from_wrapped_secret,
)


class FailingSeed:
    def unwrap_ed25519_seed(self) -> bytearray:
        raise RuntimeError("unwrap failed")

    def wrap_ed25519_seed(self) -> None:
        raise RuntimeError("wrap failed")


try:
    ed25519_signing_key_from_wrapped_secret(FailingSeed())
except ExceptionGroup as eg:
    print(eg.message)
    # "Deriving Ed25519 public key failed and failed to re-wrap Ed25519 secret..."
    print(eg.exceptions)  # (unwrap_error, wrap_error)

What just happened: When public-key derivation or signing raises and the subsequent re-wrap also raises, the helper raises an ExceptionGroup (from the exceptiongroup backport package, importable as ExceptionGroup on 3.10 or as the built-in on 3.11+) whose exceptions tuple contains both the operation error and the wrap error in order. This guarantees that neither failure is silently swallowed — both are visible to your caller. If only one of the two fails, that single error is re-raised directly. Regardless of the failure path, the unwrapped bytearray is always zeroed before the exception propagates.

Validate buffer sizes

ed25519_signing_key_from_wrapped_secret enforces the documented buffer lengths.

from algokit_crypto import (
    WrappedEd25519Seed,
    ed25519_signing_key_from_wrapped_secret,
)


class BadSeed:
    def unwrap_ed25519_seed(self) -> bytearray:
        return bytearray(31)  # wrong length

    def wrap_ed25519_seed(self) -> None:
        pass


ed25519_signing_key_from_wrapped_secret(BadSeed())
# Raises ValueError: "Expected unwrapped ed25519 seed to be 32 bytes, got 31."

What just happened: Every unwrap call is length-checked against ED25519_SEED_SIZE (32) or ED25519_EXTENDED_PRIVATE_KEY_LENGTH (96), and any mismatch is reported via ValueError with a precise message identifying the expected type and the actual length. This check runs on the initial public-key derivation and on every sign call, so a store that intermittently returns truncated buffers will fail loudly rather than producing invalid signatures.

Source: SKILL.md on GitHub

1 warning1mo3 checks · Risk SAFE
  • Gen Agent Trust Hub1mo

    The skill is a technical reference guide for the Algorand Python SDK (AlgoKit Utils). It promotes secure coding practices, including environment-based secret management and memory-safe cryptographic patterns for handling private keys. No malicious patterns or security risks were identified.

  • Socket1mo

    No alerts

  • Snyk1mo

    Risk: MEDIUM · 2 issues

Signed by skilld at 4235fb8. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 3 weeks ago.

Activeupdated 6 months ago

README badge

README badge for algorand-devrel/algorand-agent-skills/algokit-utils-py