c8ntinuum EVM transactions use standard Ethereum transaction construction, signing, raw-transaction encoding, broadcasting, and receipt polling. Use eth_sendRawTransaction to submit an offline-signed withdrawal. The JavaScript and Java examples below are excerpted from compile-tested samples. See Tested Exchange Examples.

Online and offline split

StepEnvironmentData
Fetch nonce and feesOnlineeth_getTransactionCount, eth_gasPrice, eth_maxPriorityFeePerGas, eth_estimateGas
Build unsigned transactionOnline or signing coordinatorchainId, nonce, to, value, gas fields, optional data
Sign transactionOffline signer or HSMPrivate key, unsigned transaction
Analyze signed transactionOffline and onlineHash, sender, chain ID, nonce, recipient, value, data selector
BroadcastOnlineRaw signed transaction bytes via eth_sendRawTransaction
ConfirmOnlineeth_getTransactionReceipt with status == 1
Never move private keys to the online broadcasting host. Move only unsigned transaction data into the signer and signed raw transactions out of it.

Transaction fields

FieldNative CTM transferERC-20 transfer
toRecipient accountToken contract
valueAmount in wei0
dataEmptyABI-encoded transfer(address,uint256)
gasLimitUsually 21000Use eth_estimateGas plus policy buffer
chainId2184 mainnet, 2185 testnetSame
typeDynamic fee transaction is recommendedDynamic fee transaction is recommended

JavaScript

import {
  Interface,
  JsonRpcProvider,
  Transaction,
  Wallet,
  parseEther,
  parseUnits,
} from "ethers";

const provider = new JsonRpcProvider("https://public-evm-rpc.c8ntinuum.com");
const chainId = 2184;
const privateKey = process.env.PRIVATE_KEY;
const signer = new Wallet(privateKey);

async function feeFields() {
  const feeData = await provider.getFeeData();
  return {
    maxFeePerGas: feeData.maxFeePerGas ?? feeData.gasPrice,
    maxPriorityFeePerGas: feeData.maxPriorityFeePerGas ?? 0n,
  };
}

async function signNativeTransfer(to) {
  const from = await signer.getAddress();
  const nonce = await provider.getTransactionCount(from, "pending");
  const fees = await feeFields();

  const unsignedTx = {
    type: 2,
    chainId,
    nonce,
    to,
    value: parseEther("1.0"),
    gasLimit: 21000n,
    ...fees,
  };

  const raw = await signer.signTransaction(unsignedTx);
  const decoded = Transaction.from(raw);
  console.log({
    hash: decoded.hash,
    from: decoded.from,
    chainId: decoded.chainId?.toString(),
    nonce: decoded.nonce,
    to: decoded.to,
    value: decoded.value.toString(),
  });

  return raw;
}

async function signErc20Transfer(token, recipient) {
  const from = await signer.getAddress();
  const nonce = await provider.getTransactionCount(from, "pending");
  const erc20 = new Interface(["function transfer(address to,uint256 amount) returns (bool)"]);
  const data = erc20.encodeFunctionData("transfer", [
    recipient,
    parseUnits("12.5", 18),
  ]);

  const fees = await feeFields();
  const gasLimit = await provider.estimateGas({
    from,
    to: token,
    value: 0,
    data,
  });

  return signer.signTransaction({
    type: 2,
    chainId,
    nonce,
    to: token,
    value: 0,
    data,
    gasLimit: (gasLimit * 120n) / 100n,
    ...fees,
  });
}

async function broadcast(raw) {
  const sent = await provider.broadcastTransaction(raw);
  const receipt = await sent.wait();
  if (receipt.status !== 1) throw new Error(`transaction failed: ${sent.hash}`);
  return receipt;
}

Java

import java.math.BigInteger;
import java.util.Arrays;
import org.web3j.abi.FunctionEncoder;
import org.web3j.abi.TypeReference;
import org.web3j.abi.datatypes.Address;
import org.web3j.abi.datatypes.Bool;
import org.web3j.abi.datatypes.Function;
import org.web3j.abi.datatypes.generated.Uint256;
import org.web3j.crypto.Credentials;
import org.web3j.crypto.RawTransaction;
import org.web3j.crypto.TransactionDecoder;
import org.web3j.crypto.TransactionEncoder;
import org.web3j.protocol.Web3j;
import org.web3j.protocol.core.DefaultBlockParameterName;
import org.web3j.protocol.core.methods.response.EthSendTransaction;
import org.web3j.protocol.http.HttpService;
import org.web3j.utils.Convert;
import org.web3j.utils.Numeric;

Web3j web3j = Web3j.build(new HttpService("https://public-evm-rpc.c8ntinuum.com"));
Credentials credentials = Credentials.create(System.getenv("PRIVATE_KEY"));
long chainId = 2184L;

BigInteger nonce = web3j
    .ethGetTransactionCount(credentials.getAddress(), DefaultBlockParameterName.PENDING)
    .send()
    .getTransactionCount();
BigInteger maxPriorityFeePerGas = web3j.ethMaxPriorityFeePerGas().send().getMaxPriorityFeePerGas();
BigInteger maxFeePerGas = web3j.ethGasPrice().send().getGasPrice().multiply(BigInteger.valueOf(2));

RawTransaction nativeTx = RawTransaction.createEtherTransaction(
    chainId,
    nonce,
    BigInteger.valueOf(21_000),
    "0x1111111111111111111111111111111111111111",
    Convert.toWei("1.0", Convert.Unit.ETHER).toBigInteger(),
    maxPriorityFeePerGas,
    maxFeePerGas);

byte[] signed = TransactionEncoder.signMessage(nativeTx, chainId, credentials);
String raw = Numeric.toHexString(signed);
var decoded = TransactionDecoder.decode(raw);

System.out.println("raw=" + raw);
System.out.println("hash=" + org.web3j.crypto.Hash.sha3(raw));
System.out.println("from=" + credentials.getAddress());
System.out.println("chainId=" + chainId);
System.out.println("nonce=" + decoded.getNonce());
System.out.println("to=" + decoded.getTo());
System.out.println("value=" + decoded.getValue());
System.out.println("gasLimit=" + decoded.getGasLimit());
System.out.println("maxPriorityFeePerGas=" + maxPriorityFeePerGas);
System.out.println("maxFeePerGas=" + maxFeePerGas);

EthSendTransaction sent = web3j.ethSendRawTransaction(raw).send();
String txHash = sent.getTransactionHash();
System.out.println("broadcastHash=" + txHash);

Function transfer = new Function(
    "transfer",
    Arrays.asList(new Address("0x1111111111111111111111111111111111111111"), new Uint256(new BigInteger("12500000000000000000"))),
    Arrays.asList(new TypeReference<Bool>() {}));
String data = FunctionEncoder.encode(transfer);

RawTransaction erc20Tx = RawTransaction.createTransaction(
    chainId,
    nonce.add(BigInteger.ONE),
    BigInteger.valueOf(80_000),
    "0xc8Fb80fCc03f699C70ff0CC08C09106288888888",
    BigInteger.ZERO,
    data,
    maxPriorityFeePerGas,
    maxFeePerGas);

String erc20Raw = Numeric.toHexString(
    TransactionEncoder.signMessage(erc20Tx, chainId, credentials));
var decodedErc20 = TransactionDecoder.decode(erc20Raw);
System.out.println("erc20DataSelector=" + decodedErc20.getData().substring(0, 10));

Go

package withdrawals

import (
	"context"
	"crypto/ecdsa"
	"math/big"
	"strings"

	"github.com/ethereum/go-ethereum/accounts/abi"
	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/core/types"
	"github.com/ethereum/go-ethereum/crypto"
	"github.com/ethereum/go-ethereum/ethclient"
)

var chainID = big.NewInt(2184)

func SignNativeTransfer(ctx context.Context, client *ethclient.Client, key *ecdsa.PrivateKey, to common.Address, amount *big.Int) ([]byte, common.Hash, error) {
	from := crypto.PubkeyToAddress(key.PublicKey)
	nonce, err := client.PendingNonceAt(ctx, from)
	if err != nil {
		return nil, common.Hash{}, err
	}
	tip, err := client.SuggestGasTipCap(ctx)
	if err != nil {
		return nil, common.Hash{}, err
	}
	feeCap, err := client.SuggestGasPrice(ctx)
	if err != nil {
		return nil, common.Hash{}, err
	}

	tx := types.NewTx(&types.DynamicFeeTx{
		ChainID:   chainID,
		Nonce:     nonce,
		GasTipCap: tip,
		GasFeeCap: new(big.Int).Mul(feeCap, big.NewInt(2)),
		Gas:       21_000,
		To:        &to,
		Value:     amount,
	})

	signed, err := types.SignTx(tx, types.LatestSignerForChainID(chainID), key)
	if err != nil {
		return nil, common.Hash{}, err
	}

	raw, err := signed.MarshalBinary()
	if err != nil {
		return nil, common.Hash{}, err
	}
	return raw, signed.Hash(), nil
}

func SignERC20Transfer(ctx context.Context, client *ethclient.Client, key *ecdsa.PrivateKey, token common.Address, recipient common.Address, amount *big.Int) (*types.Transaction, error) {
	erc20ABI, err := abi.JSON(strings.NewReader(`[{"name":"transfer","type":"function","inputs":[{"name":"to","type":"address"},{"name":"amount","type":"uint256"}],"outputs":[{"type":"bool"}]}]`))
	if err != nil {
		return nil, err
	}
	data, err := erc20ABI.Pack("transfer", recipient, amount)
	if err != nil {
		return nil, err
	}

	from := crypto.PubkeyToAddress(key.PublicKey)
	nonce, err := client.PendingNonceAt(ctx, from)
	if err != nil {
		return nil, err
	}
	tip, err := client.SuggestGasTipCap(ctx)
	if err != nil {
		return nil, err
	}
	feeCap, err := client.SuggestGasPrice(ctx)
	if err != nil {
		return nil, err
	}

	tx := types.NewTx(&types.DynamicFeeTx{
		ChainID:   chainID,
		Nonce:     nonce,
		GasTipCap: tip,
		GasFeeCap: new(big.Int).Mul(feeCap, big.NewInt(2)),
		Gas:       80_000,
		To:        &token,
		Value:     big.NewInt(0),
		Data:      data,
	})
	return types.SignTx(tx, types.LatestSignerForChainID(chainID), key)
}

func Broadcast(ctx context.Context, client *ethclient.Client, signed *types.Transaction) error {
	return client.SendTransaction(ctx, signed)
}

Rust

use alloy::consensus::{SignableTransaction, TxEip1559, TxEnvelope};
use alloy::eips::eip2718::Encodable2718;
use alloy::network::TxSignerSync;
use alloy::primitives::{address, Bytes, TxKind, U256};
use alloy::providers::{Provider, ProviderBuilder};
use alloy::signers::local::PrivateKeySigner;

fn build_native_tx(
    nonce: u64,
    to: alloy::primitives::Address,
    value: U256,
    max_fee_per_gas: u128,
    max_priority_fee_per_gas: u128,
) -> TxEip1559 {
    TxEip1559 {
        chain_id: 2184,
        nonce,
        gas_limit: 21_000,
        max_fee_per_gas,
        max_priority_fee_per_gas,
        to: TxKind::Call(to),
        value,
        input: Bytes::new(),
        access_list: Default::default(),
    }
}

fn sign_offline(tx: &mut TxEip1559, private_key: &str) -> eyre::Result<Vec<u8>> {
    let signer: PrivateKeySigner = private_key.parse()?;
    let signature = signer.sign_transaction_sync(tx)?;
    let signed = tx.clone().into_signed(signature);
    let envelope: TxEnvelope = signed.into();
    Ok(envelope.encoded_2718())
}

#[tokio::main]
async fn main() -> eyre::Result<()> {
    let provider = ProviderBuilder::new().on_http(
        "https://public-evm-rpc.c8ntinuum.com".parse()?,
    );

    let mut tx = build_native_tx(
        7,
        address!("0000000000000000000000000000000000000001"),
        U256::from(1_000_000_000_000_000_000u128),
        50_000_000_000,
        1_000_000_000,
    );

    let raw = sign_offline(&mut tx, &std::env::var("PRIVATE_KEY")?)?;
    let pending = provider.send_raw_transaction(&raw).await?;
    let receipt = pending.get_receipt().await?;

    if !receipt.status() {
        eyre::bail!("transaction failed");
    }

    Ok(())
}

Signed data analysis

Before broadcasting, decode the raw signed transaction and verify:
  • chainId is 2184 for mainnet or 2185 for testnet.
  • from matches the expected hot wallet, withdrawal wallet, HSM key, or custody account.
  • nonce matches the reserved nonce in your withdrawal database.
  • to, value, and data match the approved withdrawal instruction.
  • ERC-20 withdrawals call transfer(address,uint256) on the intended token contract.
  • Gas fields satisfy your policy and are not extreme.
  • The raw transaction hash is stored before broadcast.

Broadcasting and polling

Broadcast with eth_sendRawTransaction, then poll eth_getTransactionReceipt by hash until one of these happens:
ResultAction
Receipt status == 1Mark withdrawal confirmed.
Receipt status == 0Mark failed and investigate before retrying.
null receiptContinue polling until timeout.
Nonce too lowCheck if the transaction was already mined or replaced.
Transaction underpricedRebuild with a replacement transaction using the same nonce and higher fee, if policy allows.
Insufficient fundsStop retries until the sender has native CTM for value plus gas.
Store every raw transaction, decoded transaction, broadcast hash, receipt, and replacement attempt for auditability.