Skip to content

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 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: 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 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, 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:

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

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) — Wallet-as-a-Service 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 Wallet-as-a-Service
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.

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.

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 under Transaction encoding formats.

Transaction typeDescription
TokenCreateIssue a new token
TokenMintMint tokens to an address
TokenBurnBurn tokens from an address
TokenTransferTransfer tokens between addresses
TokenGrantAuthorityGrant an authority role to an address
TokenRevokeAuthorityRevoke an authority role from an address
TokenBlacklistAccountAdd an address to the token blacklist
TokenWhitelistAccountAdd an address to the token whitelist
TokenPausePause token operations
TokenUnpauseUnpause token operations
TokenUpdateMetadataUpdate the token name, URI, or metadata
BatchPaymentPay many recipients of one token in a single transaction

TokenMint, TokenBurn, and TokenTransfer

These three types share the same payload shape:

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

TokenCreate

{
  "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:

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

TokenGrantAuthority and TokenRevokeAuthority

{
  "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

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

TokenPause and TokenUnpause

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

TokenUpdateMetadata

{
  "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:

{
  "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
    {
      "sequence": "5"
    }
  2. Build the raw transaction in your application:

    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
    }
    {
      "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.

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