> For the complete documentation index, see [llms.txt](https://emeditweb.gitbook.io/pulsar-stellar-sdk/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://emeditweb.gitbook.io/pulsar-stellar-sdk/concepts/decoded-values.md).

# Decoded values

Every contract value the SDK hands you, a topic, an event's data, an element of a vector, is a `DecodedValue`. It is a discriminated union: one field, `type`, says which variant it is, and the rest of the shape follows from that. This is the taxonomy from ADR-023, and it mirrors the Soroban `ScVal` variants the decoder produces.

## The variants

| `type`                 | Shape                          | Notes                                   |
| ---------------------- | ------------------------------ | --------------------------------------- |
| `address`              | `{ value: string }`            | contract or account address, as text    |
| `symbol`               | `{ value: string }`            | a Soroban Symbol                        |
| `string`               | `{ value: string }`            | a Soroban String                        |
| `bool`                 | `{ value: boolean }`           |                                         |
| `bytes`                | `{ value: string }`            | lowercase hex, not base64               |
| `u32`                  | `{ value: number }`            | fits in a JS number                     |
| `i32`                  | `{ value: number }`            | fits in a JS number                     |
| `u64` `i64`            | `{ value: string }`            | wider than 2^53, so a string            |
| `u128` `i128`          | `{ value: string }`            | wider than 2^53, so a string            |
| `u256` `i256`          | `{ value: string }`            | wider than 2^53, so a string            |
| `timepoint` `duration` | `{ value: string }`            | 64-bit second counts, so strings        |
| `vec`                  | `{ value: DecodedValue[] }`    | nested values                           |
| `tuple`                | `{ value: DecodedValue[] }`    | indexer path only, see below            |
| `map`                  | `{ value: DecodedMapEntry[] }` | ordered pairs, see below                |
| `void`                 | `{ }`                          | no `value` field                        |
| `unknown`              | `{ xdr: string }`              | base64 XDR, no `value` field, see below |

Reading a value means narrowing on `type` first:

```ts
function amountOf(value: DecodedValue): bigint {
  if (value.type !== 'i128') throw new Error('expected an i128 amount');
  return BigInt(value.value); // value.value is a string here
}
```

Three of these variants encode a decision worth understanding.

## Wide integers are strings

A JSON number is an IEEE 754 double. It holds integers exactly only up to 2^53. A Soroban `i128` amount or a `u64` ledger sequence can exceed that, and a double rounds the excess silently, with no error to catch. So every integer wider than 32 bits, `u64` through `i256` plus `timepoint` and `duration`, comes back as a string. `u32` and `i32` stay numbers because they always fit.

Convert when you need arithmetic, and convert to `bigint`, not `number`:

```ts
const amount = BigInt(value.value); // value.type is 'i128'
```

This is the same reasoning ADR-021 applies to event ids, which are also strings.

## bytes is hex

A `bytes` value is lowercase hexadecimal, not base64. It sits next to `rawData` and `rawTopics`, which are base64 XDR, and reading a decoded byte string against its raw source is easier when the decoded form is hex. To get bytes back, `Buffer.from(value.value, 'hex')`.

## map is an ordered array, not an object

A `map` is a `DecodedMapEntry[]`, where each entry is `{ key, value }` and both sides are themselves `DecodedValue`s. It is not a JavaScript object and not a `Map`.

The reason is that Soroban map keys are `ScVal`s. A key can be a Symbol, an Address, or an integer, and two keys of different types can render to the same string. Folding the map into an object keyed by string would erase the key's type, collapse those distinct keys into one, and lose the wire ordering. The array keeps the key's type, keeps duplicates, keeps the order, and survives `JSON.stringify` unchanged.

```ts
if (value.type === 'map') {
  for (const { key, value: v } of value.value) {
    // key and v are each a DecodedValue; narrow them the same way
  }
}
```

## tuple appears only on the indexer path

A Soroban tuple is encoded on the wire exactly as a vector. Nothing in the XDR distinguishes the two; only a decoder holding the contract's spec can tell a tuple from a plain vector. The RPC path has no spec, so it emits `vec`. The `tuple` variant exists for the indexer, which can carry that distinction. If you read only from RPC, you will never see a `tuple`.

## unknown is a fallback, not an error

`decodeScVal` never throws. A variant this SDK version does not recognize, or a value whose payload does not decode, comes back as `{ type: 'unknown', xdr: '<base64>' }`, carrying its XDR intact.

This is deliberate. One malformed or novel value in a response does not discard every other event in that response. A future protocol addition degrades to an opaque value you can still see, log, and decode yourself from the `xdr`, rather than failing the whole page. Handle `unknown` as the default arm of your switch and the rest of your code keeps working across a protocol change.

## Two ways to read topics, one lossy

The SDK ships two topic decoders, and they are not interchangeable.

* **`decodeTopics(topics)`** returns `DecodedValue[]`, following every rule above. This is the faithful view.
* **`parseTopics(topics)`** returns `unknown[]` by running Stellar's own `scValToNative` on each topic. This is the ergonomic view, for inspection, logging, and ad-hoc scripting.

`parseTopics` is lossy, and the losses are Stellar's `scValToNative`, not this project's:

* A map with duplicate keys keeps only the last entry, with no error. `decodeScVal` keeps both, in order.
* A contract instance is not converted; you get the raw XDR struct back. `decodeScVal` returns it as `unknown` with its base64 intact.
* Wide integers come back as `bigint`. `decodeScVal` returns strings, so they survive JSON without rounding.
* Bytes come back as a `Buffer`. `decodeScVal` returns hex.

Use `parseTopics` when the values are ordinary and convenience is the point. Use `decodeTopics` when the result is stored, compared, or sent anywhere, because that is where a dropped map entry or a rounded integer does damage. The same choice exists at the single-value level: `scValToNative` is re-exported for convenience, and `decodeScVal` is the faithful counterpart.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://emeditweb.gitbook.io/pulsar-stellar-sdk/concepts/decoded-values.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
