# Raw signing

Raw signing with Wallet-as-a-Service (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 Wallet-as-a-Service (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 Wallet-as-a-Service (Palisade).

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

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 documentation
See our [Wallet-as-a-Service (Palisade) API reference](/products/wallet/api-docs/palisade-api/palisade-api) for information on how to submit raw transactions via the API.

## Automatic chain routing (EVM)

For EVM wallets, the platform automatically detects the target blockchain from the `chainId` field in the encoded transaction and routes to the correct connector. You don't need to specify a `blockchain` parameter — the encoded transaction is the source of truth.

- **Known chain ID** — If the chain ID matches a natively integrated chain (Ethereum, Arbitrum, Polygon, Base, BNB Chain, Avalanche, 1Money), the platform routes the transaction to that chain's connector. The platform supports both `signOnly=true` and `signOnly=false`.
- **Unknown chain ID** — If the chain ID doesn't match any natively integrated chain, the platform treats the transaction as [cross-chain](#cross-chain-raw-signing-evm): it signs only and requires a policy with an explicit `CHAIN_ID` matcher.


1Money wallets are an exception: raw transactions from a 1Money wallet always execute on 1Money, regardless of the chain ID in the encoded transaction.

Blockchain parameter deprecated
The `blockchain` field on the raw transaction request is deprecated. The platform ignores it for all raw transactions. For EVM wallets, the chain ID in the encoded transaction determines routing — a contradicting `blockchain` value is ignored with no error or warning. Raw transactions from non-EVM wallets and 1Money wallets always execute on the wallet's native blockchain.

### Example

An Ethereum wallet can sign and broadcast an Arbitrum transaction without any override:


```
POST /v2/vaults/{vaultId}/wallets/{walletId}/transactions/raw
{
  "encodedTransaction": "<RLP-encoded transaction with Arbitrum chainId>",
  "signOnly": false
}
```

The platform extracts the Arbitrum chain ID from the encoded transaction, routes to the Arbitrum connector, and broadcasts the transaction.

Fund the target chain
The platform doesn't check the wallet's balance on the target chain before broadcasting. Your address is the same on every EVM chain, but it needs the native token for gas on the target chain. If the address isn't funded there, the network rejects the transaction and it fails with an insufficient funds reason.

## Cross-chain raw signing (EVM)

For EVM wallets, you can raw-sign transactions for chain IDs that Wallet-as-a-Service (Palisade) doesn't natively integrate — for example, custom L2s, testnets, or chains Palisade hasn't yet onboarded.

Cross-chain raw signing uses the same raw transaction endpoint. When the platform detects a chain ID that doesn't match any natively integrated chain, it enforces sign-only, checks a chain-ID-scoped policy, and returns the signature. Wallet-as-a-Service doesn't broadcast the transaction — you submit the signed transaction to the target chain yourself.

### Requirements

- **EVM-only** — Available for EVM wallets only.
- **Sign-only** — The request must set `signOnly=true`. Wallet-as-a-Service rejects requests with `signOnly=false` for an unknown chain ID with HTTP 400.
- **Explicit `CHAIN_ID` matcher required** — The wallet must have a policy whose matchers include a `CHAIN_ID` matcher for the target chain ID. Base policies without matchers don't apply to cross-chain transactions — a base policy alone doesn't authorize the sign.
- **Natively integrated chain IDs rejected** — Wallet-as-a-Service rejects a `CHAIN_ID` matcher value that matches a chain it integrates natively (for example, Ethereum mainnet `1`). Use the standard raw signing flow for those chains.


### Flow

1. Create a policy on the EVM wallet that includes a `CHAIN_ID` matcher for each chain ID you want to allow. See [Policy reference](/products/wallet/user-interface/policies/policies-reference#chain_id) for the matcher format.
2. Submit a raw transaction to the standard raw signing endpoint with `signOnly=true`. Encode the transaction using the EVM format described in [Transaction Encoding Formats](#transaction-encoding-formats), setting the `chainId` field to the target chain ID.
3. The platform detects the unknown chain ID, evaluates the policy, and returns the signature on success.
4. Submit the signed transaction to the target chain yourself.


No broadcast
Cross-chain raw signing returns a signature only. Wallet-as-a-Service (Palisade) has no connector for the target chain and doesn't publish the transaction. You're responsible for broadcasting the transaction — and for funding the address with the target chain's native gas token when you submit it.

## 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:**


```python
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:**


```go
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:**


```javascript
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:**


```python
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:**


```python
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:**


```javascript
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)` — Wallet-as-a-Service (Palisade) computes this automatically


**Supported contract types:**

| Contract Type | Description |
|  --- | --- |
| `TransferContract` | Native TRX transfers |
| `TriggerSmartContract` | TRC-20 token transfers and smart contract calls |


**Python example (using tronpy):**


```python
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 Wallet-as-a-Service (Palisade)
encoded_transaction = raw_bytes.hex()

# For TRC-20 transfers, use trigger_smart_contract instead
```

**JavaScript example (using TronWeb):**


```javascript
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:**


```go
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.