Skip to content

Commit 537cb6f

Browse files
committed
Format decoded nodes into formatted node trees
1 parent 385c7a6 commit 537cb6f

10 files changed

Lines changed: 1290 additions & 11 deletions

File tree

‎packages/dynamic-codecs/README.md‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,35 @@ All formatters share the `(decoded, options)` signature and accept the following
115115
| `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. |
116116
| `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. |
117117

118+
## Formatted nodes
119+
120+
`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.
121+
122+
```ts
123+
import { assertIsFormattedNode, formatDecodedNode } from '@codama/dynamic-codecs';
124+
125+
// account: a decoded account, e.g. from `parseAccountData` of `@codama/dynamic-parsers`
126+
const formatted = formatDecodedNode(account, {
127+
formatAddress: address => names.get(address) ?? address,
128+
});
129+
formatted.text; // "Amount: 1.5 USDC, Owner: Alice, Config: (Fee: 25 bps)"
130+
131+
assertIsFormattedNode(formatted.data, 'structTypeNode');
132+
for (const field of formatted.data.fields) {
133+
field.label; // "Amount"
134+
field.text; // "1.5 USDC"
135+
field.type; // the formatted type, e.g. a struct to lay out as nested rows
136+
}
137+
```
138+
139+
- **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`.
140+
- **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.
141+
- **Enum variants** carry their `label` and `skipInnerData`.
142+
- **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.
143+
- **Degraded** nodes are amounts whose `decimals` cannot be resolved, written as their raw value, and every node whose text includes them.
144+
145+
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.
146+
118147
## Node paths
119148

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

‎packages/dynamic-codecs/src/codecs.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,6 @@ import {
8989
transformCodec,
9090
} from '@solana/codecs';
9191

92-
import { getCodecFromBytesEncoding } from './bytes';
9392
import type {
9493
DecodedAccountNode,
9594
DecodedNodeCodec,
@@ -114,6 +113,7 @@ import type {
114113
GetDecodedNode,
115114
} from './decoded';
116115
import { getLazyCodec } from './lazy';
116+
import { getCodecFromBytesEncoding } from './utils';
117117
import {
118118
assertUniqueItems,
119119
assertValueType,

‎packages/dynamic-codecs/src/decoded.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import type {
3232
ZeroableOptionTypeNode,
3333
} from '@codama/nodes';
3434
import { assertIsNodePath, isNodePath, type NodePath } from '@codama/visitors-core';
35+
import type { Address } from '@solana/addresses';
3536
import type { Codec, Option } from '@solana/codecs';
3637

3738
/**
@@ -146,7 +147,7 @@ export type DecodedOptionTypeNode = DecodedNodeBase<OptionTypeNode, Option<unkno
146147
};
147148

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

151152
/** A decoded remainder option, e.g. `{ value: { __option: 'Some', value: 42n }, item: … }`. */
152153
export type DecodedRemainderOptionTypeNode = DecodedNodeBase<RemainderOptionTypeNode, Option<unknown>> & {

‎packages/dynamic-codecs/src/format.ts‎

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
import { CODAMA_ERROR__INVALID_TICKS_PER_SECOND, CodamaError } from '@codama/errors';
2-
import { titleCase } from '@codama/fragments/casing';
32
import {
43
AmountNumberDisplayNode,
54
DateTimeTypeNode,
65
DurationTypeNode,
7-
getTextNodeContent,
86
InjectableIntegerValueNode,
97
InjectableStringValueNode,
108
InjectedValueNode,
@@ -28,7 +26,6 @@ import {
2826
Signedness,
2927
} from '@solana/codecs';
3028

31-
import { getCodecFromBytesEncoding } from './bytes';
3229
import type {
3330
DecodedBooleanTypeNode,
3431
DecodedBytesTypeNode,
@@ -41,6 +38,7 @@ import type {
4138
DecodedPublicKeyTypeNode,
4239
DecodedStringTypeNode,
4340
} from './decoded';
41+
import { getCodecFromBytesEncoding, getEnumVariantLabel } from './utils';
4442

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

267263
/**
@@ -274,8 +270,7 @@ export function formatEnum(decoded: DecodedEnumTypeNode, _options: FormatOptions
274270
* ```
275271
*/
276272
export function formatPublicKey(decoded: DecodedPublicKeyTypeNode, options: FormatOptions = {}): string {
277-
const address = decoded.value as Address;
278-
return options.formatAddress ? options.formatAddress(address) : address;
273+
return options.formatAddress ? options.formatAddress(decoded.value) : decoded.value;
279274
}
280275

281276
const SECONDS_PER_DAY = 86_400n;

0 commit comments

Comments
 (0)