Raw signing

Raw signing with Palisade offers the ability to sign any transaction type available to a blockchain. It can be used on any transaction type if enabled.

This is an important feature of the Palisade platform as it means customers can benefit from all natively supported transaction types of a blockchain as soon as they become available, regardless of whether that blockchain or transaction type is currently supported by Palisade.

Raw signing can be enabled and disabled for a wallet from wallet settings.

❗️

Important: disabled by default

Raw signing is a powerful and insecure signing method. It is therefore disabled by default. Please only enable raw signing for individual wallets if you fully understand this feature.

📘

API docs

See our Raw Signing API docs for information on how to submit raw transactions via the API.

Transaction Encoding Formats

When using the Raw Transaction API, the encodedTransaction field must be encoded in a blockchain-specific format. This section details the exact encoding requirements for each supported blockchain.

EVM Chains (Ethereum, Base, Polygon, Avalanche, Arbitrum, BNB Chain)

The encodedTransaction field must match the output of go-ethereum's rlp.EncodeToBytes(types.Transaction).

Format:

RLP(type_byte || RLP([chainId, nonce, maxPriorityFeePerGas, maxFeePerGas, gas, to, value, data, accessList, v, r, s]))

Key requirements:

  1. Include v, r, s signature fields — Set to 0 for unsigned transactions
  2. Apply outer RLP wrapper — The typed transaction must be wrapped as an RLP byte string
⚠️

Common error

If you receive "typed transaction too short", you're likely using the standard unsigned format (0x02 || RLP([9 fields])) which is missing the v/r/s placeholders and outer RLP wrapper.

Python example:

from eth_account.typed_transactions import DynamicFeeTransaction
import rlp

# Use the signed transaction serializer (includes v, r, s fields)
serializer = DynamicFeeTransaction._signed_transaction_serializer

tx = serializer(
    chainId=84532,  # Base Sepolia
    nonce=nonce,
    maxPriorityFeePerGas=max_priority,
    maxFeePerGas=max_fee,
    gas=gas_limit,
    to=bytes.fromhex(to_address[2:]),
    value=0,
    data=calldata_bytes,
    accessList=(),
    v=0,  # Placeholder for unsigned
    r=0,  # Placeholder for unsigned
    s=0,  # Placeholder for unsigned
)

# RLP encode the fields
tx_rlp = rlp.encode(tx)

# Add EIP-1559 type prefix (0x02)
typed_tx = bytes([2]) + tx_rlp

# Wrap in RLP string (matches go-ethereum's rlp.EncodeToBytes)
encoded_transaction = rlp.encode(typed_tx).hex()

Go example:

import (
    "github.com/ethereum/go-ethereum/core/types"
    "github.com/ethereum/go-ethereum/rlp"
)

tx := types.NewTx(&types.DynamicFeeTx{
    ChainID:   big.NewInt(84532),
    Nonce:     nonce,
    GasTipCap: maxPriorityFeePerGas,
    GasFeeCap: maxFeePerGas,
    Gas:       gasLimit,
    To:        &toAddress,
    Value:     big.NewInt(0),
    Data:      calldata,
})

encodedBytes, _ := rlp.EncodeToBytes(tx)
encodedTransaction := hex.EncodeToString(encodedBytes)

XRP Ledger

The encodedTransaction field must use the XRP Binary Codec format with encodeForSigning.

Format: Hex-encoded binary codec output of the transaction JSON

Key requirements:

  1. Include the SigningPubKey field with the wallet's public key
  2. Use encodeForSigning (not encode) for unsigned transactions

JavaScript example:

const { encodeForSigning } = require('ripple-binary-codec');

const tx = {
    Account: "rSourceAddress...",
    Destination: "rDestAddress...",
    Amount: "1000000",  // In drops
    Fee: "10",
    Sequence: 12345,
    SigningPubKey: "02ECE63017B0FEFC...",  // Required
    TransactionType: "Payment"
};

const encodedTransaction = encodeForSigning(tx);

Python example:

from xrpl.core.binarycodec import encode_for_signing

tx = {
    "Account": "rSourceAddress...",
    "Destination": "rDestAddress...",
    "Amount": "1000000",
    "Fee": "10",
    "Sequence": 12345,
    "SigningPubKey": "02ECE63017B0FEFC...",
    "TransactionType": "Payment"
}

encoded_transaction = encode_for_signing(tx)

Solana

The encodedTransaction field must be a base64-encoded serialized Solana transaction.

Format: base64(transaction.MarshalBinary())

Key requirements:

  1. Transaction must include a valid recent blockhash
  2. Account keys must be properly ordered (fee payer first)
  3. Use standard base64 encoding (not base58)

Python example:

from solders.transaction import Transaction
from solders.message import Message
import base64

# Build your transaction message
message = Message.new_with_blockhash(
    instructions,
    payer,
    blockhash
)

# Create unsigned transaction
tx = Transaction.new_unsigned(message)

# Serialize and base64 encode
tx_bytes = bytes(tx)
encoded_transaction = base64.b64encode(tx_bytes).decode('utf-8')

JavaScript example:

const { Transaction } = require('@solana/web3.js');

const tx = new Transaction();
tx.recentBlockhash = blockhash;
tx.feePayer = payerPublicKey;
tx.add(instruction);

// Serialize (without signing)
const serialized = tx.serialize({ requireAllSignatures: false });
const encodedTransaction = serialized.toString('base64');

TRON

The encodedTransaction field must be a hex-encoded TRON Transaction protobuf.

Format: hex(proto.Marshal(Transaction))

Key requirements:

  1. Transaction must include valid ref_block_bytes and ref_block_hash from a recent block
  2. Expiration timestamp must be in the future (typically current time + 60 seconds, in milliseconds)
  3. Fee limit must be set for TRC-20 transfers (recommended: 15,000,000 SUN = 15 TRX)
  4. The signing hash is SHA256(raw_data) — Palisade computes this automatically

Supported contract types:

Contract TypeDescription
TransferContractNative TRX transfers
TriggerSmartContractTRC-20 token transfers and smart contract calls

Python example (using tronpy):

from tronpy import Tron

client = Tron(network='shasta')  # or 'mainnet'

# Build a TRX transfer transaction
txn = (
    client.trx.transfer(
        from_="TSourceAddress...",
        to="TDestAddress...",
        amount=1_000_000  # 1 TRX in SUN
    )
    .fee_limit(1_000_000)  # 1 TRX fee limit
    .build()
)

# Get the raw transaction bytes (protobuf serialized)
raw_bytes = txn._raw_data.SerializeToString()

# Hex encode for Palisade
encoded_transaction = raw_bytes.hex()

# For TRC-20 transfers, use trigger_smart_contract instead

JavaScript example (using TronWeb):

const TronWeb = require('tronweb');

const tronWeb = new TronWeb({
    fullHost: 'https://api.shasta.trongrid.io',  // or mainnet
});

// Build a TRX transfer transaction
const tx = await tronWeb.transactionBuilder.sendTrx(
    'TDestAddress...',      // to
    1000000,                // amount in SUN (1 TRX)
    'TSourceAddress...'     // from
);

// The transaction object contains raw_data_hex
const encodedTransaction = tx.raw_data_hex;

Go example:

import (
    "encoding/hex"
    "time"
    
    "github.com/fbsobreira/gotron-sdk/pkg/proto/core"
    "google.golang.org/protobuf/proto"
    "google.golang.org/protobuf/types/known/anypb"
)

// Build a TRX transfer
transfer := &core.TransferContract{
    OwnerAddress: ownerAddrBytes,  // 21-byte TRON address
    ToAddress:    toAddrBytes,
    Amount:       1000000,         // 1 TRX in SUN
}

anyValue, _ := anypb.New(transfer)

tx := &core.Transaction{
    RawData: &core.TransactionRaw{
        Contract: []*core.Transaction_Contract{{
            Type:      core.Transaction_Contract_TransferContract,
            Parameter: anyValue,
        }},
        RefBlockBytes: refBlockBytes,  // From recent block
        RefBlockHash:  refBlockHash,
        Expiration:    time.Now().Add(60*time.Second).UnixMilli(),
        Timestamp:     time.Now().UnixMilli(),
    },
}

txBytes, _ := proto.Marshal(tx)
encodedTransaction := hex.EncodeToString(txBytes)
📘

TRON address format

TRON addresses can be in base58 format (starts with T) or hex format (starts with 41). The API accepts base58 addresses, but internally they are converted to 21-byte hex addresses in the protobuf.

⚠️

Reference block requirements

TRON transactions require ref_block_bytes and ref_block_hash from a recent block (within ~18 hours). If you're building transactions manually, fetch the latest block and extract bytes 6-8 of the block number and bytes 8-16 of the block hash.