Feature Request: Add encoding/decoding commands for Soroban types in Stellar CLI
What problem does your feature solve?
Developers frequently need to convert between different Soroban type representations, especially when using tools like Stellar Lab that require exact hex encoding. Currently, there's an inconsistency: Stellar CLI automatically handles type conversions during contract invocations (e.g., accepting "ABCD" for a BytesN<32> parameter), but there's no standalone command to perform these conversions independently.
Common scenarios: converting strings to BytesN<32> hex format for use in Stellar Lab, encoding addresses to bytes, decoding hex values back to human-readable formats, preparing parameters for contract calls or inspection.
What would you like to see?
Encoding/decoding commands added to the Stellar CLI:
# Encode example
stellar contract encode --type bytes-n-32 --value "ABCD"
# Output: 4142434400000000000000000000000000000000000000000000000000000000
# Decode example
stellar contract decode --type bytes-n-32 --value "4142434400000000000000000000000000000000000000000000000000000000"
# Output: ABCD (or hex if not valid UTF-8)
This would reduce friction when switching between CLI and Lab and would improve developer experience.
What alternatives are there?
Using Python/Node/Unix commands, custom scripts or online converters.
I'm surprised that ascii is accepted and interpreted for a bytes value, that might be a bug, it should only accept hex as well, otherwise it's unclear to me how you would pass hex to bytes that aren't ascii.
For other type conversions what would be some good examples for how it would work? Is the goal to convert from the loose JS types that the stellar-cli and js-stellar-sdk use to and from ScVal values?
Simple example demonstrating inconsistent behavior when invoking a contract function that accepts a BytesN<32> parameter:
#![no_std]
use soroban_sdk::{contract, contractimpl, BytesN, Env};
#[contract]
pub struct Contract;
#[contractimpl]
impl Contract {
pub fn hello(env: Env, to: BytesN<32>) -> bool {
true
}
}
Stellar Lab accepts the hex-encoded format but rejects the string format (as expected):
- Works: 4142434400000000000000000000000000000000000000000000000000000000
- Fails: ABCD
Prepare: HostError: Error(WasmVm, InvalidAction)
Event log (newest first):
0: [Diagnostic Event] contract:..., topics:[error, Error(WasmVm, InvalidAction)],
data:["VM call trapped: UnreachableCodeReached", hello]
1: [Diagnostic Event] topics:[fn_call, ..., hello], data:Bytes(abcd)
However, CLI Tool shows the opposite behavior: Works: ABCD
stellar contract invoke --id ID --source ACCOUNT --network testnet --send=no -- hello --to ABCD
Fails: 4142434400000000000000000000000000000000000000000000000000000000
stellar contract invoke --id ID --source ACCOUNT --network testnet --send=no -- hello --to 4142434400000000000000000000000000000000000000000000000000000000
error: parsing argument to: value is not parseable to Some(
BytesN(
ScSpecTypeBytesN {
n: 32,
},
),
)
In general, the encode and decode commands would allow developers to provide loose values (such as strings, numbers, arrays, or maps) and convert them into properly typed Soroban values for contract calls, or decode contract results back into a human-readable form.
It could support all Soroban built-in types, including:
- Primitives: strings, symbols, booleans, integers (various sizes)
- Binary types: Bytes, BytesN
- Complex types: vectors, maps, addresses, options
- Nested structures
Suggested principles:
- Intuitive input formats: JSON arrays/objects, strings for addresses, hex for bytes
- Explicit typing: A
--typeflag allows users to disambiguate when multiple encodings are possible - Clear errors: Invalid conversions (e.g., decoding random hex as an address, or encoding a too long string into BytesN<32>) should return a “not convertible” or “not supported” error
- Copy-friendly output: Encoded values should be emitted in a format that can be reused directly in RPC calls or Stellar Lab (e.g., hex for BytesN)
Example:
stellar contract encode --type Bytes32 "ABCD"
stellar contract decode --type Bytes32 "4142434400000000000000000000000000000000000000000000000000000000"
The most common case will likely be converting ASCII text to/from bytes, but the tool could also cover other structures to simplify contract interaction workflows.
An open question is whether the CLI in this case should behave exactly like the Stellar SDK (for consistency across tools) or define its own conventions.
Just focusing on the inconsistency for a second, using this contract as an example, this is what I see with stellar-cli 23.1.3:
#![no_std]
use soroban_sdk::{contract, contractimpl, BytesN};
#[contract]
pub struct Contract;
#[contractimpl]
impl Contract {
pub fn echo(b: BytesN<N: 32>) -> BytesN<N: 32> {
b
}
}
ABCD works as an input because it is valid hex, and the stellar-cli sees that the hex value is supposed to be 32 bytes and right-aligns into it zero padding the result. This is probably a bad idea that it does this and it should just require the fixed length. It looks like it is accepting and converting the ASCII to bytes, but it is not.
$ stellar contract invoke --id contract -- echo --b ABCD
ℹ️ Simulation identified as read-only. Send by rerunning with `--send=yes`.
"000000000000000000000000000000000000000000000000000000000000abcd"
Hex is accepted:
$ stellar contract invoke --id contract -- echo --b 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
ℹ️ Simulation identified as read-only. Send by rerunning with `--send=yes`.
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
So it looks like the way that the Lab and the CLI differ is that the Lab does not accept partial hex values and does not pad for fixed length byte values. I think the appropriate change here should probably be to change the CLI to not do the magic padding too. cc @fnando
Opened an issue to track this bug:
- https://github.com/stellar/stellar-cli/issues/2244
the encode and decode commands would allow developers to provide loose values (such as strings, numbers, arrays, or maps) and convert them into properly typed Soroban values for contract calls, or decode contract results back into a human-readable form.
👍🏻. And this was the intent with the way in the CLI that numbers convert to and from all the number types automatically, and strings convert to and from strings or symbols automatically, and that arrays and objects can be represented with JSON-like structures. But I don't think the implementation is perfect and it has some gaps. This is an area that the CLI could tighten up on, and a specification may help solidify it I think.
Your example about zero-padding explains why the CLI accepts ABCD as a valid BytesN<32> hex value. However, I'm still puzzled why "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" succeed while "4142434400000000000000000000000000000000000000000000000000000000" fails? Both are properly formatted 64-chars hex strings representing 32 bytes.
Interestingly, this value, for example, works without issues: "414243440000000000000000000000000000000000000000000000000000000a"
This seems like a bug.
This seems like a bug.
Agreed. Opened an issue for that bug:
- https://github.com/stellar/stellar-cli/issues/2245