Skip to content

When you use the raw transaction API, the encodedTransaction field must be encoded in a blockchain-specific format. This page details the exact encoding requirements for each supported blockchain.

The API decodes encodedTransaction when you submit it. A payload it can't decode returns HTTP 400 and creates no transaction: on an EVM wallet, the message is invalid encoded transaction with the reason PAL010.011, and on an XRP Ledger wallet, xrp ledger orchestrator rejected the transaction with the reason PAL010.022. A request without encodedTransaction returns HTTP 400 with the reason PAL000.020.

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 be an XRP Binary Codec signing payload: the output of encodeForSigning for single-signing, or encodeForMultisigning for multi-signing. Only these payloads produce valid signatures. The platform checks the signing prefix after it signs, and fails a transaction whose payload has neither prefix.

Format: Hex-encoded binary codec signing payload of the transaction JSON

Key requirements:

  1. For single-signed transactions, set SigningPubKey to the wallet's public key
  2. Use encodeForSigning (not encode) for unsigned transactions
  3. For multi-signed transactions, use encodeForMultisigning with the signer's address, leave SigningPubKey empty, and set signOnly: true
encode() output fails

Ripple Custody (Wallet) signs exactly the bytes you submit. XRPL verifies each signature against a signing payload that starts with a 4-byte prefix: encodeForSigning prepends 53545800 (STX\0) for single-signing, and encodeForMultisigning prepends 534D5400 (SMT\0) for multi-signing. Plain encode adds neither. If you submit encode output, the request returns HTTP 200, but the transaction ends in FAILED with the reason SignProcessor: unsupported signing payload prefix. A multi-signing payload (534D5400) submitted with signOnly: false also ends in FAILED, because the platform can't submit a multi-signed transaction.

Quick check: confirm that your encodedTransaction hex starts with 53545800 (single-sign) or 534D5400 (multi-sign).

Multi-signing: Build the transaction's Signers array from the returned canonicalSignature. The platform assembles single-signed blobs only, so for a 534D5400 payload the response carries canonicalSignature but no signedTransaction or hash.

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);

// For multi-signed transactions, set SigningPubKey to "" and pass the signer's address:
// const encodedTransaction = encodeForMultisigning({ ...tx, SigningPubKey: "" }, signerAddress);

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)

# For multi-signed transactions, set SigningPubKey to "" and pass the signer's address:
# encoded_transaction = encode_for_multisigning({**tx, "SigningPubKey": ""}, signer_address)

Java example (xrpl4j):

import org.xrpl.xrpl4j.codec.binary.XrplBinaryCodec;

XrplBinaryCodec codec = XrplBinaryCodec.getInstance();

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

// Single-signing: prepends 53545800
String encodedTransaction = codec.encodeForSigning(txJson);

// For multi-signed transactions, pass the signer's address
// (and leave SigningPubKey empty in the transaction JSON):
// String encodedTransaction = codec.encodeForMultiSigning(txJson, signerAddress);

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) — Ripple Custody (Wallet) computes this automatically

Supported contract types:

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

Python example (using the TRON HTTP API):

import requests

# Build a TRX transfer transaction
tx = requests.post(
    "https://api.shasta.trongrid.io/wallet/createtransaction",  # or mainnet
    json={
        "owner_address": "TSourceAddress...",
        "to_address": "TDestAddress...",
        "amount": 1_000_000,  # 1 TRX in SUN
        "visible": True,
    },
).json()

# raw_data_hex is only the raw_data message. Wrap it as field 1 of a
# Transaction protobuf: tag 0x0a, varint length, then the raw_data bytes.
raw = bytes.fromhex(tx["raw_data_hex"])
length, n = b"", len(raw)
while n >= 0x80:
    length += bytes([(n & 0x7F) | 0x80])
    n >>= 7
length += bytes([n])

encoded_transaction = (b"\x0a" + length + raw).hex()

# For TRC-20 transfers, use wallet/triggersmartcontract 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
);

// raw_data_hex is only the raw_data message. Wrap it as field 1 of a
// Transaction protobuf: tag 0x0a, varint length, then the raw_data bytes.
const raw = Buffer.from(tx.raw_data_hex, 'hex');
const length = [];
for (let n = raw.length; ; n >>>= 7) {
    if (n < 0x80) { length.push(n); break; }
    length.push((n & 0x7f) | 0x80);
}

const encodedTransaction = Buffer.concat([Buffer.from([0x0a, ...length]), raw]).toString('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.

1Money

The encodedTransaction field must be a hex-encoded JSON object with a transactionType and a payload.

Format: hex(JSON({ transactionType, payload }))

{
  "transactionType": "TokenMint",
  "payload": {
    "chain_id": 1212101,
    "nonce": 0,
    "recipient": "0x...",
    "value": 1000000000000000000,
    "token": "0x..."
  }
}

Key requirements:

  1. Set transactionType to one of the supported transaction types. (See 1Money transaction types.)
  2. Include the 1Money chain_id in every payload
  3. Set nonce to the wallet's current sequence, from GET /v2/vaults/{vaultId}/wallets/{walletId}/sequence. (See Example: mint tokens.)
  4. Use snake_case payload field names, as shown in the examples.
Payload shapes vary by transaction type

The envelope is the same for every 1Money transaction, but the payload fields differ. Check 1Money transaction types before encoding — several types share a shape, and a few (notably BatchPayment) don't.