tonutils-go

repository·master·Indexed 20 days ago

https://github.com/xssnick/tonutils-go

A native Go implementation of TON (The Open Network) blockchain protocols, including ADNL and the Lite protocol. It provides a concurrent-safe connection pool for interacting with TON lite servers, supporting DHT, RLDP, and core blockchain operations such as wallet management, TON transfers, contract GET methods, and external messages. The library includes specialized clients for NFTs, Jettons, and DNS records, as well as a TLB loader for struct serialization and tools for creating and validating Merkle Proofs.

Tokens
10.4K
Snippets
34
Records
44
Agent score
64%

What's inside tonutils-go

  1. Use TLB Loader for struct serialization

    master

    Instead of manual cell assembly using cell.BeginCell(), use TLB tags to map Go structs directly to TON cells. This is more error-prone to do manually and TLB is the recommended way for application-level code.

    Common TLB Tags:

    • tlb:"#hex": Exact hex magic/constructor.
    • tlb:"## N": Fixed-width integer of N bits.
    • tlb:"addr": TON address (*address.Address).
    • tlb:"^": Referenced cell (nested value in a different cell).
    • tlb:".": Nested struct in the current cell.
    • tlb:"maybe <tag>": Optional value (presence bit + value).
    • tlb:"either X Y": Selector bit (0 for X, 1 for Y).
    • tlb:"dict N": Dictionary with N-bit keys.
    • tlb:"?FieldName <tag>": Conditional field based on a previously declared boolean FieldName.

    Use tlb.Parse(&struct, cell) to decode and tlb.ToCell(struct) to encode.

    type ExamplePayload struct {
        _       tlb.Magic  `tlb:"#a1b2c3d4"` // Magic prefix
        QueryID uint64     `tlb:"## 64"`     // 64-bit uint
        Flags   uint8      `tlb:"## 8"`      // 8-bit uint
        Body    *cell.Cell `tlb:"^"`         // Referenced cell
    }
    
    // Parsing
    var payload ExamplePayload
    if err := tlb.Parse(&payload, payloadCell); err != nil {
        panic(err)
    }
    
    // Serializing
    payloadCell, err := tlb.ToCell(payload)
  2. How BLS12-381 on-curve points are handled in TVM

    master

    The TON Virtual Machine (TVM) requires BLS12-381 opcodes (like BLS_G1_ADD, BLS_G1_MUL, BLS_PAIRING, and BLS_AGGREGATE) to accept points that are on the curve but outside the prime-order r-torsion subgroup. This behavior matches the reference C++ node implementation.

    Standard implementations like github.com/cloudflare/circl/ecc/bls12381 reject off-subgroup points during decoding via G1.SetBytes or G2.SetBytes. To support TON's requirements, this project uses specialized methods that relax the validation from IsOnG1/IsOnG2 (which checks for r-torsion) to a simpler isValidProjective && isOnCurve check.

  3. Create and validate Merkle Proofs

    master

    The library supports two ways to create proofs:

    1. Low-level ProofSkeleton: Used when you know the exact reference indices you want to keep.
    2. MerkleProofBuilder (Recommended): Used when you want the proof to follow the path of data you actually loaded from a dictionary, slice, or nested structure. It automatically prunes unloaded branches.

    To verify a proof, use cell.CheckProof(merkleProof, hash) or cell.UnwrapProof(merkleProof, hash) to continue reading the proof body.

    // Recommended: MerkleProofBuilder
    dict := cell.NewDict(32)
    // ... populate dict ...
    
    root := dict.AsCell()
    proofBuilder := cell.NewMerkleProofBuilder(root)
    observed := proofBuilder.Root().AsDict(32)
    
    // Load value (this marks the path in the builder)
    loaded, _ := observed.LoadValue(key)
    // ... parse loaded value ...
    
    // Create the proof
    proof, err := proofBuilder.CreateProof()
    
    // Verify
    err = cell.CheckProof(proof, root.Hash())
  4. Connect to the TON blockchain

    master

    To interact with the TON blockchain, you must first establish a connection using a connection pool. You can use public liteservers from the official TON configuration JSON files. The library manages a pool of connections and performs load balancing across them.

    Note on Consistency: Because the library uses a connection pool, different requests might hit different nodes. If a node hasn't applied a specific block yet, you might encounter errors. To ensure all requests in a specific operation are routed to the same node, use client.StickyContext(context.Background()) and pass the resulting context to your API methods.

    client := liteclient.NewConnectionPool()
    
    configUrl := "https://ton-blockchain.github.io/testnet-global.config.json"
    err := client.AddConnectionsFromConfigUrl(context.Background(), configUrl)
    if err != nil {
        panic(err)
    }
    api := ton.NewAPIClient(client).WithRetryTimeout(0, 5*time.Second)
    
    // To ensure consistency (all requests to the same node):
    ctx := client.StickyContext(context.Background())
    // Use 'ctx' in subsequent API calls
  5. Manage wallets and perform transfers

    master

    You can use existing wallets or generate new ones using wallet.NewSeed(). The library automatically handles contract deployment and initialization upon the first sent message.

    Key operations:

    • Initialize: Use wallet.FromSeedWithOptions with a seed phrase and wallet version (e.g., wallet.V3).
    • Check Balance: Use w.GetBalance(ctx, block) where block is obtained from api.CurrentMasterchainInfo.
    • Transfer: Use w.Transfer to send TON to an address with an optional comment.
    • Custom Messages: Use w.Send to send any tlb.InternalMessage to any contract.
    words := strings.Split("birth pattern ...", " ")
    
    w, err := wallet.FromSeedWithOptions(api, words, wallet.V3)
    if err != nil {
        panic(err)
    }
    
    block, err := api.CurrentMasterchainInfo(context.Background())
    if err != nil {
        panic(err)
    }
    
    balance, err := w.GetBalance(context.Background(), block)
    if err != nil {
        panic(err)
    }
    
    if balance.Nano().Uint64() >= 3000000 {
        addr := address.MustParseAddr("EQCD39VS5jcptHL8vMjEXrzGaRcCVYto7HUn4bpAOg8xqB2N")
        err = w.Transfer(context.Background(), addr, tlb.MustFromTON("0.003"), "Hey bro, happy birthday!")
        if err != nil {
            panic(err)
        }
    }
  6. How Continuation works in DHT lookups

    master

    The Continuation type is a mechanism to facilitate deep lookups in the DHT, particularly for overlays. When a FindValue or FindOverlayNodes operation is performed, the DHT might return a list of nodes that were checked but did not contain the final value.

    By capturing the *Continuation returned by these methods and passing it into the next call, you instruct the client to skip the nodes already checked (checkedNodes) and continue the search from the next logical nodes in the DHT topology. This prevents redundant network queries and allows for efficient traversal of distributed data.

  7. Understand the Toncenter Response Format

    master

    The client handles Toncenter's JSON responses. Depending on the API version used, the response structure varies:

    Standard Response (V2 and below): Requests return a JSON envelope with an ok boolean. If ok is true, the data is contained in the result field. If false, an error string is provided.

    {
      "ok": true,
      "result": { ... }
    }

    V3 Response: When using V3-compatible endpoints, the client expects the response to map directly to the target type T without the ok/result envelope wrapper.

  8. How RLDP queries and transfers work

    master

    RLDP (Remote Location Data Protocol) uses a request-response model built on top of partitioned data transfers.

    1. Queries: A query is encapsulated in a Query object containing an ID, a timeout, and the data.
    2. Transfers: Large data (like query results) are split into parts and sent via activeTransfer. This involves FEC (Forward Error Correction) using either RaptorQ or RoundRobin depending on the payload size and configuration.
    3. IDs: Transfers use 32-byte IDs. To facilitate matching requests with responses, the protocol uses a 'reversed' ID mechanism (reverseTransferId) where the expected transfer ID is the bitwise NOT of the original ID.
  9. Initialize an RLDP client

    master

    To interact with the Remote Location Data Protocol (RLDP), create a client using NewClient or NewClientV2. You must provide an implementation of the ADNL interface. NewClientV2 is recommended for modern implementations as it enables V2 protocol features.

    Note that the client manages its own background goroutines for recovery and state cleanup, so ensure you call Close() when finished to release resources.

    import "github.com/xssnick/tonutils-go/adnl/rldp"
    
    // Assuming 'a' is an existing ADNL implementation
    client := rldp.NewClientV2(a)
    defer client.Close()
  10. Initialize and run a Lite server

    master

    To create a Lite server, use NewServer with a list of ed25519.PrivateKey objects. These keys are used to identify the server during the ADNL handshake. Once initialized, call Listen(addr) to start accepting TCP connections on the specified address. You must provide a query handler using SetQueryHandler to process incoming requests, otherwise, the server will log an error and ignore queries.

    Note: SetQueryHandler is called synchronously from the connection read loop. To prevent blocking the server's ability to read new messages, perform heavy processing in a separate goroutine or worker pool.

    import (
    	"context"
    	"crypto/ed25519"
    	"github.com/xssnick/tonutils-go/liteclient"
    )
    
    // 1. Initialize server with keys
    server := liteclient.NewServer([]ed25519.PrivateKey{myPrivateKey})
    
    // 2. Set the query handler
    server.SetQueryHandler(func(ctx context.Context, client *liteclient.ServerClient, queryID []byte, query tl.Serializable) {
    	// Handle query asynchronously to avoid blocking the read loop
    	go func() {
    		// ... process query ...
    		client.Answer(queryID, response)
    	}()
    })
    
    // 3. Start listening
    err := server.Listen("0.0.0.0:6080")
    if err != nil {
    	panic(err)
    }
  11. Initialize a DHT Server

    master

    To start a Distributed Hash Table (DHT) server, use NewServer or NewServerFromConfig. A server requires a Gateway (to handle network connections), an ed25519.PrivateKey (the server's identity), a list of bootstrap Nodes, and a ValueStore for local data persistence. If no ValueStore is provided, a default in-memory store is used.

    Use NewServerFromConfig when you have a liteclient.GlobalConfig object, which simplifies setting up the network ID, K/A parameters, and bootstrap nodes.

    // Using NewServer
    server, err := dht.NewServer(gateway, privateKey, nodes, store)
    
    // Using NewServerFromConfig
    server, err := dht.NewServerFromConfig(gateway, privateKey, globalConfig, store)
  12. Initialize a DHT Client

    master

    To interact with the Distributed Hash Table (DHT), you must first initialize a Client. You can create a client using a gateway and a configuration URL, a gateway and a liteclient.GlobalConfig, or a gateway and a manual list of Node objects.

    Common initialization methods include:

    • NewClientFromConfigUrl: Fetches configuration from a URL and initializes the client.
    • NewClientFromConfig: Initializes the client using a provided liteclient.GlobalConfig.
    • NewClient: Initializes a client with a list of nodes using default parameters.
    // Example: Initialize from a config URL
    client, err := dht.NewClientFromConfigUrl(ctx, gateway, "https://example.com/config")
    if err != nil {
        // handle error
    }
    defer client.Close()