# Raw signing

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

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 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: cross-chain EVM routing

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 doesn't natively integrate — for example, custom L2s, testnets, or chains Wallet-as-a-Service 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 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 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 endpoint doesn't check for a signing prefix and signs plain `encode` output verbatim.

**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 produces invalid signatures
Wallet-as-a-Service (Palisade) 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 and the transaction reaches `SIGNED`, but XRPL rejects the result with `Invalid signature`: with `signOnly: false`, the transaction ends in `FAILED`; with `signOnly: true`, you receive a signature that XRPL rejects.

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, ignore the returned `signedTransaction` and `hash`.

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

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

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

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

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

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

### 1Money

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

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

```json
{
  "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](#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](#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](#1money-transaction-types) before encoding — several
types share a shape, and a few (notably `BatchPayment`) don't.

## 1Money transaction types

1Money is a stablecoin-native chain, so its tokens don't follow ERC-20 or SPL. Token operations are expressed as explicit transaction types rather than contract calls, and you access them through raw signing.

Encode each of the payloads below using the envelope described in
[1Money](#1money) under Transaction encoding formats.

| Transaction type | Description |
|  --- | --- |
| `TokenCreate` | Issue a new token |
| `TokenMint` | Mint tokens to an address |
| `TokenBurn` | Burn tokens from an address |
| `TokenTransfer` | Transfer tokens between addresses |
| `TokenGrantAuthority` | Grant an authority role to an address |
| `TokenRevokeAuthority` | Revoke an authority role from an address |
| `TokenBlacklistAccount` | Add an address to the token blacklist |
| `TokenWhitelistAccount` | Add an address to the token whitelist |
| `TokenPause` | Pause token operations |
| `TokenUnpause` | Unpause token operations |
| `TokenUpdateMetadata` | Update the token name, URI, or metadata |
| `BatchPayment` | Pay many recipients of one token in a single transaction |


### TokenMint, TokenBurn, and TokenTransfer

These three types share the same payload shape:

```json
{
  "transactionType": "TokenMint",
  "payload": {
    "chain_id": 1212101,
    "nonce": 0,
    "recipient": "0x1234567890123456789012345678901234567890",
    "value": 1000000000000000000,
    "token": "0xabcdef0123456789abcdef0123456789abcdef01"
  }
}
```

### TokenCreate

```json
{
  "transactionType": "TokenCreate",
  "payload": {
    "chain_id": 1212101,
    "nonce": 0,
    "symbol": "MYTOKEN",
    "name": "My Token",
    "decimals": 18,
    "master_authority": "0x1234567890123456789012345678901234567890",
    "is_private": false
  }
}
```

The response metadata for a `TokenCreate` transaction includes the new token's contract address:

```json
{
  "transaction_identifier": {
    "hash": "0x..."
  },
  "metadata": {
    "token_contract_address": "0x1234567890123456789012345678901234567890"
  }
}
```

### TokenGrantAuthority and TokenRevokeAuthority

```json
{
  "transactionType": "TokenGrantAuthority",
  "payload": {
    "chain_id": 1212101,
    "nonce": 0,
    "action": "Grant",
    "authority_type": "MasterMintBurn",
    "authority_address": "0x1234567890123456789012345678901234567890",
    "token": "0xabcdef0123456789abcdef0123456789abcdef01",
    "value": 0
  }
}
```

Authority types: `MasterMintBurn`, `MintBurn`, `Pause`, `List`, `MetadataUpdate`, `BridgeMint`.

### TokenBlacklistAccount and TokenWhitelistAccount

```json
{
  "transactionType": "TokenBlacklistAccount",
  "payload": {
    "chain_id": 1212101,
    "nonce": 0,
    "action": "Add",
    "address": "0x1234567890123456789012345678901234567890",
    "token": "0xabcdef0123456789abcdef0123456789abcdef01"
  }
}
```

### TokenPause and TokenUnpause

```json
{
  "transactionType": "TokenPause",
  "payload": {
    "chain_id": 1212101,
    "nonce": 0,
    "action": "Pause",
    "token": "0xabcdef0123456789abcdef0123456789abcdef01"
  }
}
```

### TokenUpdateMetadata

```json
{
  "transactionType": "TokenUpdateMetadata",
  "payload": {
    "chain_id": 1212101,
    "nonce": 0,
    "name": "Updated Token Name",
    "uri": "https://example.com/token-metadata",
    "token": "0xabcdef0123456789abcdef0123456789abcdef01",
    "additional_metadata": []
  }
}
```

### BatchPayment

`BatchPayment` pays many recipients of a single token in one transaction:

```json
{
  "transactionType": "BatchPayment",
  "payload": {
    "chain_id": 1212101,
    "nonce": 0,
    "token": "0xabcdef0123456789abcdef0123456789abcdef01",
    "operations": [
      { "recipient": "0x1234567890123456789012345678901234567890", "amount": 1500000 },
      { "recipient": "0xa0B640D215622e72198D1BFf88aCd8aFB9198844", "amount": 2500000 }
    ],
    "created_at": 1755090000
  }
}
```

Key behaviors:

- **`operations` is ordered, and the order is significant** — it forms part of the signed bytes.
- **`created_at` is caller-supplied Unix seconds** — the SDK doesn't stamp a clock of its own. It also forms part of the signed bytes, so use the same value throughout signing and submission.
- **`operations_hash` and `batch_id` are optional trailing fields.** Leaving `operations_hash` unset is always valid. If you set it, it must equal the canonical hash of `operations` (use `DeriveBatchPaymentOperationsHash`) or the node rejects the transaction.
- **Policies evaluate the sum of the operation amounts**, not a per-recipient figure, so a transaction limit must cover the whole batch. Each token contract needs its own configured limit — assets are keyed on symbol, blockchain, and contract address.
- **These payloads fail locally before signing** rather than at the node: an empty `operations` list, a zero-address recipient, a zero or absent `amount`, amounts totalling more than 2^256−1, and an `operations_hash` that doesn't match `operations`.


BatchPayment signs a different digest
Unlike the other transaction types, `BatchPayment` exists only in the SDK's domain-separated v2 format and has no legacy v1 equivalent. The connector handles this — you don't need to do anything differently — but a client configured with the 1Money SDK's `WithLegacyV1` option can't send one.

### Example: mint tokens

1. Get the current nonce for your wallet:

```
GET /v2/vaults/{vaultId}/wallets/{walletId}/sequence
```

```json
{
  "sequence": "5"
}
```
2. Build the raw transaction in your application:

```go
payload := onemoney.TokenMintPayload{
    ChainID:   1212101,
    Nonce:     5,  // from step 1
    Recipient: common.HexToAddress("0xa0B640D215622e72198D1BFf88aCd8aFB9198844"),
    Value:     big.NewInt(1000000000000000000), // 1 token (18 decimals)
    Token:     common.HexToAddress("0x6b01a50120ebcd864a0376375d911a001395a062"),
}

rawTx := map[string]interface{}{
    "transactionType": "TokenMint",
    "payload":         payload,
}

// Marshal to JSON and hex encode
jsonBytes, _ := json.Marshal(rawTx)
encodedTx := hex.EncodeToString(jsonBytes)
```
3. Submit the raw transaction:

```
POST /v2/vaults/{vaultId}/wallets/{walletId}/transactions/raw
Content-Type: application/json

{
  "encodedTransaction": "7b227472616e73616374696f6e54797065223a22546f6b656e4d696e74222c...",
  "signOnly": false
}
```

```json
{
  "id": "tx-uuid-here",
  "status": "PENDING_APPROVAL",
  "asset": {
    "id": "asset-uuid",
    "symbol": "BOB"
  }
}
```
4. Poll the transaction status until it's confirmed. See
[Manage transactions](/products/wallet/user-interface/transactions/manage-transactions).

```
GET /v2/vaults/{vaultId}/wallets/{walletId}/transactions/{txId}
```