Skip to main content
This tutorial will give you a tiny, working scaffold that shows how to package instruction data and forward it to another program with tsys_invoke. The example compiles but leaves business logic as placeholders so you can extend it for your real payloads.

Prerequisites

Minimal instruction shape (CPI stub)

The header defines a pared-down instruction struct: only indices plus a small payload that you can repurpose. Add or replace fields to match your program.
programs/c/examples/tn_cpi_stub_program.h
#ifndef HEADER_tn_programs_c_examples_tn_cpi_stub_program_h
#define HEADER_tn_programs_c_examples_tn_cpi_stub_program_h

#include <thru-sdk/c/tn_sdk.h>

#define TN_CPI_STUB_INSTRUCTION (0U) /* Tag understood by the callee */

#define TN_CPI_STUB_ERR_INVALID_INPUT (1UL)
#define TN_CPI_STUB_ERR_UNSUPPORTED   (2UL)

/* Placeholder-friendly instruction forwarded to another program. */
typedef struct __attribute__((packed)) {
  uint    instruction_type;
  ushort  target_program_account_idx;
  ushort  mint_account_idx;
  ushort  authority_account_idx;
  uchar   payload[8]; /* Replace with your real data */
} tn_cpi_stub_args_t;

#endif /* HEADER_tn_programs_c_examples_tn_cpi_stub_program_h */

Minimal entrypoint with tsys_invoke

This entrypoint validates indices, builds a tiny buffer, and forwards it. Swap the buffer layout for your real instruction body.
programs/c/examples/tn_cpi_stub_program.c
#include <stddef.h>
#include <string.h>

#include <thru-sdk/c/tn_sdk.h>
#include <thru-sdk/c/tn_sdk_syscall.h>

#include "tn_cpi_stub_program.h"

TSDK_ENTRYPOINT_FN void
start( uchar const * instruction_data,
       ulong         instruction_data_sz ) {
  if( instruction_data_sz < sizeof( tn_cpi_stub_args_t ) ) {
    tsdk_revert( TN_CPI_STUB_ERR_INVALID_INPUT );
  }

  tn_cpi_stub_args_t const * args =
      (tn_cpi_stub_args_t const *)instruction_data;

  if( !tsdk_is_account_idx_valid( args->target_program_account_idx ) ||
      !tsdk_is_account_idx_valid( args->mint_account_idx ) ||
      !tsdk_is_account_idx_valid( args->authority_account_idx ) ) {
    tsdk_revert( TN_CPI_STUB_ERR_INVALID_INPUT );
  }

  /* Minimal payload: tag + mint index + authority index + one byte */
  uchar token_instr[1 + sizeof( ushort ) + sizeof( ushort ) + sizeof( uchar )];
  ulong offset = 0UL;

  token_instr[offset] = TN_CPI_STUB_INSTRUCTION;
  offset++;
  memcpy( token_instr + offset, &args->mint_account_idx, sizeof( ushort ) );
  offset += sizeof( ushort );
  memcpy( token_instr + offset, &args->authority_account_idx, sizeof( ushort ) );
  offset += sizeof( ushort );
  token_instr[offset] = args->payload[0];
  offset++;

  ulong invoke_err;
  ulong invoke_result = tsys_invoke( token_instr,
                                     offset,
                                     args->target_program_account_idx,
                                     &invoke_err );

  if( invoke_result != TSDK_SUCCESS ) tsdk_revert( invoke_result );
  if( invoke_err    != TSDK_SUCCESS ) tsdk_revert( invoke_err );

  tsdk_return( TSDK_SUCCESS );
}

How the pieces fit

  • Payload layout: The buffer starts with a single-byte tag (TN_CPI_STUB_INSTRUCTION), followed by the two indices and the first payload byte. Replace this layout with your own wire format.
  • Invocation: tsys_invoke receives the buffer pointer, its length (offset), and the target program account index. Both syscall errors (invoke_result) and callee errors (invoke_err) are bubbled up with tsdk_revert.
  • Account ordering reminder: idx0 is the fee payer, idx1 is the current program; the rest are whatever you supply in the transaction. Make sure the indices you place in the struct match the order in the transaction header.
This example is deliberately minimal and does not perform real work. Always align your payload and account indices with the program you are calling, and validate any variable-length data before forwarding it.