Codama Documentation

repository·main·Indexed 19 days ago

https://github.com/codama-idl/codama

Codama is a tool for describing Solana programs using a standardized Codama IDL format, serving as a single source of truth to generate multi-language program clients, documentation, and tooling. It includes a CLI for initializing configurations and running visitor scripts, as well as specialized packages like @codama/dynamic-address-resolution for resolving instruction account addresses and PDAs, and @codama/dynamic-codecs for retrieving codecs for IDL nodes.

Tokens
124.1K
Snippets
519
Records
633
Agent score
66%

What's inside Codama

  1. What is a Fragment in Codama?

    main

    A Fragment is a building block used by Codama renderers to compose generated source code. It solves the problem of managing code snippets and their dependencies simultaneously.

    Instead of just a string, a fragment carries:

    1. content: The actual source code string.
    2. imports: (In flavored fragments) An ImportMap that tracks the symbolic dependencies required by the content.

    When fragments are interpolated into one another using the fragment tagged template, the imports propagate upwards automatically. This allows you to build small pieces (like a single field) and compose them into larger structures (like a struct or a file) without manually threading import statements through every helper function.

    import { fragment, use } from '@codama/fragments/javascript';
    
    const pubkey = use('type Address', '@solana/kit');
    const interfaceFragment = fragment`
    export interface Account {
        readonly owner: ${pubkey};
    }
    `;
    
    console.log(interfaceFragment.content);
    // export interface Account {
    //     readonly owner: Address;
    // }
    
    console.log(interfaceFragment.imports);
    // Map { '@solana/kit' => Map { 'Address' => { ..., isType: true } } }
  2. What is a RenderMap and how to use it

    main

    A RenderMap is a utility type that manages a collection of Fragment files to be rendered. It acts as an intermediary between content generation logic and filesystem writing logic. This allows for testing generated code in environments without a Filesystem API (like browsers) and facilitates testing without actual disk I/O.

    Key characteristics:

    • It is essentially a JavaScript Map.
    • Paths inside a RenderMap should be relative to the base directory used when writing to the filesystem.
    • RenderMaps are immutable; updates return a new instance.
    // Example of creating a RenderMap with multiple files
    const renderMap = createRenderMap({
        'path/to/file.ts': { content: 'file content' },
        'path/to/another/file.ts': { content: 'another file content' },
    });
  3. Overview of Codama IDL Nodes

    main

    The Codama IDL is composed of various nodes that describe different aspects of a Solana program. These nodes are categorized by their purpose. For example, nodes describing data structures that can be encoded/decoded into buffers are grouped under the TypeNode category.

    You can refer to any specific node using the Node helper type provided by the package.

  4. Understand the `LinkNode` abstraction

    main

    In Codama, LinkNode is an abstract type helper used to represent any node that establishes a link to another node.

    Important: LinkNode is a type alias and cannot be used directly as a node in your code. To implement or use a linking node, you must use one of the specific concrete implementations provided by the library.

  5. Resolve link nodes using `LinkableDictionary`

    main

    A LinkableDictionary allows you to store and access linkable nodes (like ProgramNodes, AccountNodes, or PdaNodes) via their link nodes.

    To use it effectively:

    1. Use recordLinkablesOnFirstVisitVisitor to populate the dictionary before any nodes are visited.
    2. Use recordNodeStackVisitor to keep track of the current path.
    3. Use the dictionary methods to resolve links to their actual nodes.
    const linkables = new LinkableDictionary();
    
    // Record linkable nodes via their full path.
    linkables.recordPath([rootNode, programNode, accountNode]);
    
    // Get a linkable node using the full path of a link node
    const programNode: ProgramNode | undefined = linkables.get([...somePath, programLinkNode]);
    
    // Get a linkable node or throw if not found
    const programNode: ProgramNode = linkables.getOrThrow([...somePath, programLinkNode]);
    
    // Get the path of a linkable node
    const accountPath: NodePath<AccountNode> | undefined = linkables.getPath([...somePath, accountLinkNode]);
    
    // Get the path of a linkable node or throw if not found
    const accountPath: NodePath<AccountNode> = linkables.getPathOrThrow([...somePath, accountLinkNode]);
  6. How to use visitors in configuration

    main

    Visitors are the core building blocks of Codama scripts. You can reference them in your configuration using several patterns:

    1. Import Paths

    Visitors must be provided via an import path (local or NPM package). By default, the default export is used.

    • Local: './my-visitor.js' or '/abs/path/to/visitor.js'
    • Package: 'some-library' or '@acme/some-library'

    To import a named export, append # followed by the name:

    • './my-visitor.js#myExport'
    • '@acme/some-library#myExport'

    2. Visitors with Arguments

    If a visitor is a function that accepts arguments, use an object instead of a string:

    • from (string): The import path (and optional #export).
    • args (array): Arguments to pass to the visitor function.
    {
        "from": "@acme/some-library#myExport",
        "args": ["hello", { "someOption": true }]
    }

    3. Chaining and Transformation

    If a visitor returns a new RootNode, that new node is passed to the next visitor in the chain. This allows you to transform the IDL in stages.

    export default {
        scripts: {
            documentation: [
                './delete-all-accounts.js', // Returns IDL without accounts
                './generate-documentation.js', // Operates on the modified IDL
            ],
        },
    };
  7. Configure the discriminator size for EnumTypeNode

    main

    The size attribute of an EnumTypeNode determines the size of the discriminator used during serialization. This discriminator is a NumberTypeNode that prepends the serialized variant data.

    By default, Codama uses the variant's index (0, 1, 2...) as the discriminator. If you need a specific discriminator size (e.g., a u8 or u32), pass it via the options object in the enumTypeNode helper function.

    // Example: Using a u32 discriminator instead of the default index
    const node = enumTypeNode(variants, { size: numberTypeNode('u32') });
  8. Understand the ValueNode type helper

    main

    In Codama, ValueNode is an abstract type helper used to represent the collection of all available value nodes. It is important to note that ValueNode is a type alias and cannot be instantiated or used directly as a node in your AST (Abstract Syntax Tree).

    To represent specific data values, you must use one of the concrete node implementations instead of the ValueNode alias.

  9. How to build a renderer using Visitors and RenderMaps

    main

    When building a renderer, the recommended pattern is to create a visitor that traverses the Codama IDL and returns a RenderMap. This allows for testing the logic without filesystem side effects.

    1. Testable approach: Use a visitor that returns a RenderMap (e.g., Visitor<RenderMap>).
    2. Writing to disk: Wrap that visitor in writeRenderMapVisitor(visitor, baseDir) to execute the write.
    3. High-level approach: For production renderers, provide a visitor that handles directory cleanup (e.g., using deleteDirectory) before writing.
    import { deleteDirectory } from '@codama/renderers-core';
    import { rootNodeVisitor, visit } from '@codama/visitors-core';
    
    // Example of a high-level renderer setup
    export function renderVisitor(path: string, options: { deleteFolderBeforeRendering?: boolean } = {}) {
        return rootNodeVisitor(async root => {
            if (options.deleteFolderBeforeRendering ?? true) {
                deleteDirectory(path);
            }
            // Assuming getRenderMapVisitor() is your core logic
            visit(root, writeRenderMapVisitor(getRenderMapVisitor(), path));
        });
    }
  10. Use PayerValueNode to represent the paying wallet

    main

    A PayerValueNode is used to represent the primary wallet responsible for paying for transactions. This is useful for defining default values for instruction accounts, such as specifying that a 'payer' account should default to the user's wallet in a web app or a specific address in a CLI.

    Note that PayerValueNode is distinct from IdentityValueNode. While they are often the same wallet, PayerValueNode specifically denotes the entity that pays for things, whereas IdentityValueNode denotes the entity that owns things.

  11. How PreOffsetTypeNode works

    main

    A PreOffsetTypeNode is a wrapper for another TypeNode that shifts the encoding/decoding cursor by a specified amount before the child node is processed. This is primarily used to move the encoded value of the child node itself or to create NestedTypeNodes.

    If you need the offset to be applied after the child node has been encoded/decoded, use PostOffsetTypeNode instead.

    const relativeOffsetNode = preOffsetTypeNode(numberTypeNode('u32'), 2);