Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions packages/dynamic-codecs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,35 @@ All formatters share the `(decoded, options)` signature and accept the following
| `numberFormat` | `Intl.NumberFormat` | Format numbers for a locale, e.g. `"1,234,567.89"`. Its options decide the digits shown. Amounts and fixed points are given to it exactly, never through a JavaScript float. |
| `resolveInjectedValue` | `(path: NodePath<InjectedValueNode>) => unknown` | Resolve injected values of display nodes, e.g. `decimals` provided by an instruction, from their path through the decoded node. `undefined` means unresolved. |

## Formatted nodes

`formatDecodedNode` formats a decoded node of any kind, e.g. a whole account, into a formatted node: the same tree, where every node also carries its `text` and whether it is `degraded`. Formatted nodes are decoded nodes too, so they keep their `path`, `value` and offsets and work wherever decoded nodes do.

```ts
import { assertIsFormattedNode, formatDecodedNode } from '@codama/dynamic-codecs';

// account: a decoded account, e.g. from `parseAccountData` of `@codama/dynamic-parsers`
const formatted = formatDecodedNode(account, {
formatAddress: address => names.get(address) ?? address,
});
formatted.text; // "Amount: 1.5 USDC, Owner: Alice, Config: (Fee: 25 bps)"

assertIsFormattedNode(formatted.data, 'structTypeNode');
for (const field of formatted.data.fields) {
field.label; // "Amount"
field.text; // "1.5 USDC"
field.type; // the formatted type, e.g. a struct to lay out as nested rows
}
```

- **Leaves** use the formatter of their kind. On top of the [formatting options](#formatting), you can override the formatters of booleans, bytes, date-times, durations, enum labels, fixed points, floats, integers and strings, e.g. `formatBoolean: boolean => (boolean.value ? 'Yes' : 'No')`. Public keys go through `formatAddress`.
- **Struct fields** carry their `label`, `skip`, `flatten` and `flattenPrefix`, from their display node or their defaults: the identifier in title case, `"never"`, `false` and `""`. `flatten` is only `true` when the field's type is a struct.
- **Enum variants** carry their `label` and `skipInnerData`.
- **Text** joins the text of children: `Label: text` for struct fields, `, ` between items and map entries, `None` for empty options, and the variant's label followed by its data in parentheses for enums, e.g. `Limit (Price: 100)`. Containers within containers are wrapped in parentheses, so an empty one shows as `()`, e.g. `Items: ()`. Struct fields skipped `always` are left out, and flattened fields are replaced by their own fields, with their prefix.
- **Degraded** nodes are amounts whose `decimals` cannot be resolved, written as their raw value, and every node whose text includes them.

Laying out the tree, e.g. as rows honouring `flatten` and `skip`, is left to you. To fill a sentence such as an instruction's interpolated intent, `getDecodedNodeAtPath` finds the node a path expression points to, e.g. `getDecodedNodeAtPath(formatted, pathString('config.fees[0]'))?.text`. It returns `undefined` for indices beyond the items of an array or set, and throws `CODAMA_ERROR__CANNOT_RESOLVE_PATH` for paths that do not fit the data, e.g. an unknown field. As with `resolveTypePath`, paths do not go through options or enums.

## Node paths

The full path is needed to resolve link nodes, which may point to other programs, and injected values, which are provided by the enclosing instructions.
Expand Down
2 changes: 1 addition & 1 deletion packages/dynamic-codecs/src/codecs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,6 @@ import {
transformCodec,
} from '@solana/codecs';

import { getCodecFromBytesEncoding } from './bytes';
import type {
DecodedAccountNode,
DecodedNodeCodec,
Expand All @@ -114,6 +113,7 @@ import type {
GetDecodedNode,
} from './decoded';
import { getLazyCodec } from './lazy';
import { getCodecFromBytesEncoding } from './utils';
import {
assertUniqueItems,
assertValueType,
Expand Down
3 changes: 2 additions & 1 deletion packages/dynamic-codecs/src/decoded.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import type {
ZeroableOptionTypeNode,
} from '@codama/nodes';
import { assertIsNodePath, isNodePath, type NodePath } from '@codama/visitors-core';
import type { Address } from '@solana/addresses';
import type { Codec, Option } from '@solana/codecs';

/**
Expand Down Expand Up @@ -146,7 +147,7 @@ export type DecodedOptionTypeNode = DecodedNodeBase<OptionTypeNode, Option<unkno
};

/** A decoded public key, e.g. `{ value: '11111111111111111111111111111111' }`. */
export type DecodedPublicKeyTypeNode = DecodedNodeBase<PublicKeyTypeNode, string>;
export type DecodedPublicKeyTypeNode = DecodedNodeBase<PublicKeyTypeNode, Address>;

/** A decoded remainder option, e.g. `{ value: { __option: 'Some', value: 42n }, item: … }`. */
export type DecodedRemainderOptionTypeNode = DecodedNodeBase<RemainderOptionTypeNode, Option<unknown>> & {
Expand Down
11 changes: 3 additions & 8 deletions packages/dynamic-codecs/src/format.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { CODAMA_ERROR__INVALID_TICKS_PER_SECOND, CodamaError } from '@codama/errors';
import { titleCase } from '@codama/fragments/casing';
import {
AmountNumberDisplayNode,
DateTimeTypeNode,
DurationTypeNode,
getTextNodeContent,
InjectableIntegerValueNode,
InjectableStringValueNode,
InjectedValueNode,
Expand All @@ -28,7 +26,6 @@ import {
Signedness,
} from '@solana/codecs';

import { getCodecFromBytesEncoding } from './bytes';
import type {
DecodedBooleanTypeNode,
DecodedBytesTypeNode,
Expand All @@ -41,6 +38,7 @@ import type {
DecodedPublicKeyTypeNode,
DecodedStringTypeNode,
} from './decoded';
import { getCodecFromBytesEncoding, getEnumVariantLabel } from './utils';

/**
* Options shared by the formatters of decoded nodes. Every formatter accepts them, even those
Expand Down Expand Up @@ -259,9 +257,7 @@ export function formatBytes(decoded: DecodedBytesTypeNode, _options: FormatOptio
* ```
*/
export function formatEnum(decoded: DecodedEnumTypeNode, _options: FormatOptions = {}): string {
const variant = getLastNodeFromPath(decoded.variant.path);
const label = variant.display?.label;
return label === undefined ? titleCase(variant.identifier) : getTextNodeContent(label);
return getEnumVariantLabel(decoded.variant);
}

/**
Expand All @@ -274,8 +270,7 @@ export function formatEnum(decoded: DecodedEnumTypeNode, _options: FormatOptions
* ```
*/
export function formatPublicKey(decoded: DecodedPublicKeyTypeNode, options: FormatOptions = {}): string {
const address = decoded.value as Address;
return options.formatAddress ? options.formatAddress(address) : address;
return options.formatAddress ? options.formatAddress(decoded.value) : decoded.value;
}

const SECONDS_PER_DAY = 86_400n;
Expand Down
Loading
Loading