Skip to content

Commit 50bd2bc

Browse files
committed
Return decoded nodes from dynamic parsers
1 parent 62035f2 commit 50bd2bc

4 files changed

Lines changed: 242 additions & 110 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ export type EncodableNodes =
139139

140140
/** Options shared by {@link getNodeCodec}, {@link getNodeValueCodec} and their visitors. */
141141
export type CodecVisitorOptions = {
142-
/** The encoding used to decode plain bytes, e.g. `["base64", "SGVsbG8="]`. Defaults to `base64`. */
142+
/** The encoding of decoded plain bytes, e.g. `'base16'` decodes them as `["base16", "0102"]`. Defaults to `base64`. */
143143
bytesEncoding?: BytesEncoding;
144144
};
145145

‎packages/dynamic-parsers/README.md‎

Lines changed: 45 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -18,84 +18,82 @@ pnpm install @codama/dynamic-parsers
1818
> [!NOTE]
1919
> This package is **not** included in the main [`codama`](../library) package.
2020
21-
## Types
21+
## Decoded nodes
2222

23-
### `ParsedData<TNode>`
24-
25-
This type represents the result of identifying and parsing a byte array from a given root node. It provides us with the full `NodePath` of the identified node, as well as the data deserialized from the provided bytes.
23+
All parsers return the [decoded nodes](../dynamic-codecs/README.md#decoded-nodes) of `@codama/dynamic-codecs`: the `path` of the identified node, the decoded `value`, the `preOffset` and `postOffset` of the cursor, and the decoded nodes of its `data`, absent for instructions without data.
2624

2725
```ts
28-
type ParsedData<TNode extends AccountNode | EventNode | InstructionNode> = {
29-
data: unknown;
30-
path: NodePath<TNode>;
31-
};
26+
const account = parseAccountData(rootNode, bytes);
27+
account?.path; // [rootNode, programNode, accountNode]
28+
account?.value; // { discriminator: 1n, owner: '9BbW...ftkT', amount: 42n }
29+
account?.data; // the decoded struct, with its `fields`
3230
```
3331

34-
The `data` is decoded using [`getNodeValueCodec`](../dynamic-codecs) from `@codama/dynamic-codecs`, so it uses the same value format. For instance, integers are `bigint`s, struct keys are the raw field identifiers and enums are `{ __kind, __discriminator, data }` objects.
32+
The `value` uses the value format of [`getNodeValueCodec`](../dynamic-codecs/README.md#value-format). For instance, integers are `bigint`s, struct keys are the raw field identifiers and enums are `{ __kind, __discriminator, data }` objects. The decoded nodes of the `data` keep the node of each value, e.g. to [format](../dynamic-codecs/README.md#formatting) it.
3533

36-
```ts
37-
const parsedData = parseAccountData(rootNode, bytes);
38-
parsedData?.data; // { discriminator: 1n, owner: '9BbW...ftkT', amount: 42n }
39-
```
34+
All parsers return `undefined` when the bytes are unparsable: either nothing is identified, or the identified node cannot decode them, e.g. truncated bytes whose discriminator matches.
4035

4136
## Functions
4237

43-
### `parseAccountData(rootNode, bytes)`
38+
### `parseAccountData(rootNode, bytes, options?)`
4439

45-
Given a `RootNode` and a byte array, this function will attempt to identify the correct account node and use it to deserialize the provided bytes. Therefore, it returns a `ParsedData<AccountNode>` object if the parsing was successful, or `undefined` otherwise.
40+
Identifies the account node of the provided bytes and decodes them with it.
4641

4742
```ts
48-
const parsedData = parseAccountData(rootNode, bytes);
49-
// ^ ParsedData<AccountNode> | undefined
50-
51-
if (parsedData) {
52-
const accountNode: AccountNode = getLastNodeFromPath(parsedData.path);
53-
const decodedData: unknown = parsedData.data;
54-
}
43+
const account = parseAccountData(rootNode, bytes);
44+
// ^ DecodedAccountNode | undefined
5545
```
5646

57-
### `parseEventData(rootNode, bytes)`
47+
### `parseEventData(rootNode, bytes, options?)`
5848

59-
Similarly to `parseAccountData`, this function will match the provided bytes to an event node and deserialize them accordingly. It returns a `ParsedData<EventNode>` object if the parsing was successful, or `undefined` otherwise.
49+
Identifies the event node of the provided bytes and decodes them with it.
6050

6151
```ts
62-
const parsedData = parseEventData(rootNode, bytes);
63-
// ^ ParsedData<EventNode> | undefined
64-
65-
if (parsedData) {
66-
const eventNode: EventNode = getLastNodeFromPath(parsedData.path);
67-
const decodedData: unknown = parsedData.data;
68-
}
52+
const event = parseEventData(rootNode, bytes);
53+
// ^ DecodedEventNode | undefined
6954
```
7055

71-
### `parseInstructionData(rootNode, bytes)`
56+
### `parseInstructionData(rootNode, bytes, options?)`
7257

73-
Similarly to `parseAccountData`, this function will match the provided bytes to an instruction node and deserialize them accordingly. It returns a `ParsedData<InstructionNode>` object if the parsing was successful, or `undefined` otherwise.
58+
Identifies the instruction node of the provided bytes and decodes them with it.
7459

7560
```ts
76-
const parsedData = parseInstructionData(rootNode, bytes);
77-
// ^ ParsedData<InstructionNode> | undefined
61+
const instruction = parseInstructionData(rootNode, bytes);
62+
// ^ DecodedInstructionNode | undefined
63+
```
7864

79-
if (parsedData) {
80-
const instructionNode: InstructionNode = getLastNodeFromPath(parsedData.path);
81-
const decodedData: unknown = parsedData.data;
82-
}
65+
### `parseData(rootNode, bytes, kind?, options?)`
66+
67+
Identifies the node of the given kind(s) of the provided bytes and decodes them with it. All kinds are searched by default.
68+
69+
```ts
70+
const parsed = parseData(rootNode, bytes, ['accountNode', 'eventNode']);
71+
// ^ DecodedAccountNode | DecodedEventNode | undefined
8372
```
8473

85-
### `parseInstruction(rootNode, instruction)`
74+
All the functions above accept the following options.
75+
76+
| Name | Type | Description |
77+
| ---------------- | --------------- | ---------------------------------------------------------------------------------------------------------------- |
78+
| `programAddress` | `string` | Restricts the search to the programs matching this address. See [Program selection](#program-selection). |
79+
| `bytesEncoding` | `BytesEncoding` | The encoding of decoded plain bytes, e.g. `'base16'` decodes them as `["base16", "0102"]`. Defaults to `base64`. |
80+
81+
### `parseInstruction(rootNode, instruction, options?)`
8682

87-
This function accepts a `RootNode` and an `Instruction` type — as defined in `@solana/instructions` — in order to return a `ParsedData<InstructionNode>` object that also includes an `accounts` array that match each `AccountMeta` with its corresponding account name.
83+
Parses an `Instruction`, as defined in `@solana/instructions`. It returns a `ParsedInstruction`: the decoded instruction node, plus its named `accounts`, each `AccountMeta` carrying the `identifier` of its instruction account, and the `remainingAccounts` beyond them, e.g. the signers of a multisig.
8884

8985
```ts
90-
const parsedData = parseInstruction(rootNode, instruction);
86+
const parsed = parseInstruction(rootNode, instruction);
87+
// ^ ParsedInstruction | undefined
9188

92-
if (parsedData) {
93-
const namedAccounts = parsedData.accounts;
94-
// ^ Array<AccountMeta & { name: string }>
89+
if (parsed) {
90+
parsed.value; // { amount: 42n }
91+
parsed.accounts; // [{ address, role, identifier: 'source' }, ...]
92+
parsed.remainingAccounts; // [{ address, role }, ...]
9593
}
9694
```
9795

98-
Note that it uses the instruction's `programAddress` to restrict the search to the matching program — including any of the root node's `additionalPrograms`. When no program of the root matches that address, nothing is parsed: matching an unknown program against another program's candidates would confidently misattribute the data, which matters when the result is displayed to end users (e.g. clear signing).
96+
It uses the instruction's `programAddress` to restrict the search to the matching program, including any of the root node's `additionalPrograms`. When no program of the root matches that address, nothing is parsed: matching an unknown program against another program's candidates would confidently misattribute the data, which matters when the result is displayed to end users (e.g. clear signing). It accepts the `bytesEncoding` option.
9997

10098
## Discriminators
10199

@@ -110,7 +108,7 @@ Accounts, events and instructions are identified using their `discriminators`, w
110108
All functions above search the root node's main program as well as its `additionalPrograms`, in that order. Additionally, they all accept an optional `programAddress` option that restricts the search to the programs matching that address. When no program matches, nothing is identified.
111109

112110
```ts
113-
const parsedData = parseInstructionData(rootNode, bytes, { programAddress: address });
111+
const instruction = parseInstructionData(rootNode, bytes, { programAddress: address });
114112
```
115113

116114
The single-non-discriminated-candidate fallback follows the same selection when a `programAddress` is provided. Without one, it stays conservative and only applies to the main program, since bytes alone cannot tell which program a discriminator-less candidate belongs to.
Lines changed: 88 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
1-
import { getNodeValueCodec, ReadonlyUint8Array } from '@codama/dynamic-codecs';
2-
import { AccountNode, EventNode, GetNodeFromKind, IdentifierString, InstructionNode, RootNode } from '@codama/nodes';
1+
import {
2+
CodecVisitorOptions,
3+
DecodedAccountNode,
4+
DecodedEventNode,
5+
DecodedInstructionNode,
6+
getNodeCodec,
7+
GetDecodedNodeFromKind,
8+
ReadonlyUint8Array,
9+
} from '@codama/dynamic-codecs';
10+
import { AccountNode, EventNode, IdentifierString, InstructionNode, RootNode } from '@codama/nodes';
311
import { getLastNodeFromPath, NodePath } from '@codama/visitors-core';
412
import type {
513
AccountLookupMeta,
@@ -14,51 +22,85 @@ import { identifyData, IdentifyDataOptions } from './identify';
1422
type ParsableNode = AccountNode | EventNode | InstructionNode;
1523
type ParsableNodeKind = ParsableNode['kind'];
1624

17-
export type ParsedData<TNode extends ParsableNode> = {
18-
data: unknown;
19-
path: NodePath<TNode>;
20-
};
25+
/** Options of the parsers: how to identify the bytes and how to decode them. */
26+
export type ParseDataOptions = CodecVisitorOptions & IdentifyDataOptions;
2127

28+
/**
29+
* Identify the account of some bytes and decode them with it.
30+
*
31+
* @example
32+
* ```ts
33+
* const account = parseAccountData(root, bytes);
34+
* account?.value; // { discriminator: 1n, amount: 42n }
35+
* ```
36+
*
37+
* @see {@link parseData}
38+
*/
2239
export function parseAccountData(
2340
root: RootNode,
2441
bytes: ReadonlyUint8Array | Uint8Array,
25-
options: IdentifyDataOptions = {},
26-
): ParsedData<AccountNode> | undefined {
42+
options: ParseDataOptions = {},
43+
): DecodedAccountNode | undefined {
2744
return parseData(root, bytes, 'accountNode', options);
2845
}
2946

47+
/**
48+
* Identify the event of some bytes and decode them with it.
49+
*
50+
* @see {@link parseData}
51+
*/
3052
export function parseEventData(
3153
root: RootNode,
3254
bytes: ReadonlyUint8Array | Uint8Array,
33-
options: IdentifyDataOptions = {},
34-
): ParsedData<EventNode> | undefined {
55+
options: ParseDataOptions = {},
56+
): DecodedEventNode | undefined {
3557
return parseData(root, bytes, 'eventNode', options);
3658
}
3759

60+
/**
61+
* Identify the instruction of some bytes and decode them with it.
62+
*
63+
* @see {@link parseData}
64+
*/
3865
export function parseInstructionData(
3966
root: RootNode,
4067
bytes: ReadonlyUint8Array | Uint8Array,
41-
options: IdentifyDataOptions = {},
42-
): ParsedData<InstructionNode> | undefined {
68+
options: ParseDataOptions = {},
69+
): DecodedInstructionNode | undefined {
4370
return parseData(root, bytes, 'instructionNode', options);
4471
}
4572

73+
/**
74+
* Identify the account, event or instruction of some bytes and decode them with it, into a
75+
* decoded node carrying its path, its value and the decoded nodes of its data.
76+
*
77+
* Returns `undefined` when the bytes are unparsable: either nothing is identified, or the
78+
* identified node cannot decode them, e.g. truncated bytes whose discriminator matches.
79+
*
80+
* @example
81+
* ```ts
82+
* const parsed = parseData(root, bytes, ['accountNode', 'eventNode']);
83+
* parsed?.path; // [root, program, account]
84+
* parsed?.value; // { discriminator: 1n, amount: 42n }
85+
* parsed?.data; // the decoded struct, with its `fields`
86+
* ```
87+
*/
4688
export function parseData<TKind extends ParsableNodeKind>(
4789
root: RootNode,
4890
bytes: ReadonlyUint8Array | Uint8Array,
4991
kind?: TKind | TKind[],
50-
options: IdentifyDataOptions = {},
51-
): ParsedData<GetNodeFromKind<TKind>> | undefined {
92+
options: ParseDataOptions = {},
93+
): GetDecodedNodeFromKind<TKind> | undefined {
5294
const path = identifyData<TKind>(
5395
root,
5496
bytes,
5597
kind ?? (['accountNode', 'instructionNode', 'eventNode'] as TKind[]),
56-
options,
98+
{ programAddress: options.programAddress },
5799
);
58100
if (!path) return undefined;
59-
const codec = getNodeValueCodec(path as NodePath<ParsableNode>);
101+
const codec = getNodeCodec(path as NodePath<ParsableNode>, { bytesEncoding: options.bytesEncoding });
60102
try {
61-
return { data: codec.decode(bytes), path };
103+
return codec.decode(bytes) as GetDecodedNodeFromKind<TKind>;
62104
} catch {
63105
// A discriminator can match while the full data does not conform (e.g. truncated or
64106
// corrupt bytes). Parsing is total: data that cannot be decoded is not parsable,
@@ -67,39 +109,50 @@ export function parseData<TKind extends ParsableNodeKind>(
67109
}
68110
}
69111

112+
/** The named accounts of a parsed instruction, each pairing its `AccountMeta` with its identifier. */
113+
export type ParsedInstructionAccounts = ReadonlyArray<AccountMeta & { identifier: IdentifierString }>;
114+
70115
/**
71-
* The parsed accounts of an instruction, each pairing its on-chain `AccountMeta` with its node name.
116+
* A parsed instruction: its decoded node, plus its named {@link ParsedInstructionAccounts} and
117+
* the `remainingAccounts` beyond them, e.g. the signers of a multisig.
72118
*/
73-
export type ParsedInstructionAccounts = ReadonlyArray<AccountMeta & { name: IdentifierString }>;
119+
export type ParsedInstruction = DecodedInstructionNode & {
120+
readonly accounts: ParsedInstructionAccounts;
121+
readonly remainingAccounts: readonly (AccountLookupMeta | AccountMeta)[];
122+
};
74123

75124
/**
76-
* A parsed instruction: its decoded data and node path, plus its resolved {@link ParsedInstructionAccounts}.
125+
* Parse an instruction, as defined in `@solana/instructions`: identify it within the program
126+
* of its `programAddress`, decode its data and name its accounts.
127+
*
128+
* @example
129+
* ```ts
130+
* const parsed = parseInstruction(root, instruction);
131+
* parsed?.value; // { amount: 42n }
132+
* parsed?.accounts; // [{ address, role, identifier: 'source' }, …]
133+
* parsed?.remainingAccounts; // [{ address, role }, …]
134+
* ```
77135
*
78-
* `remainingAccounts` carries the concrete account metas beyond the instruction's named
79-
* accounts — the ones described by the instruction node's `remainingAccounts` groups (e.g.
80-
* multisig signers). Optional so hand-built values remain valid; `parseInstruction` always
81-
* sets it.
136+
* @see {@link parseData}
82137
*/
83-
export type ParsedInstruction = ParsedData<InstructionNode> & {
84-
accounts: ParsedInstructionAccounts;
85-
remainingAccounts?: readonly (AccountLookupMeta | AccountMeta)[];
86-
};
87-
88138
export function parseInstruction(
89139
root: RootNode,
90140
instruction: Instruction &
91141
InstructionWithAccounts<readonly (AccountLookupMeta | AccountMeta)[]> &
92142
InstructionWithData<ReadonlyUint8Array>,
143+
options: CodecVisitorOptions = {},
93144
): ParsedInstruction | undefined {
94-
const parsedData = parseInstructionData(root, instruction.data, { programAddress: instruction.programAddress });
95-
if (!parsedData) return undefined;
96-
const instructionNode = getLastNodeFromPath(parsedData.path);
97-
const namedAccounts = instructionNode.accounts ?? [];
145+
const decoded = parseInstructionData(root, instruction.data, {
146+
bytesEncoding: options.bytesEncoding,
147+
programAddress: instruction.programAddress,
148+
});
149+
if (!decoded) return undefined;
150+
const namedAccounts = getLastNodeFromPath(decoded.path).accounts ?? [];
98151
const accounts: ParsedInstructionAccounts = namedAccounts.flatMap((account, index) => {
99152
const accountMeta = instruction.accounts[index];
100153
if (!accountMeta) return [];
101-
return [{ ...accountMeta, name: account.identifier }];
154+
return [{ ...accountMeta, identifier: account.identifier }];
102155
});
103156
const remainingAccounts = instruction.accounts.slice(namedAccounts.length);
104-
return { ...parsedData, accounts, remainingAccounts };
157+
return { ...decoded, accounts, remainingAccounts };
105158
}

0 commit comments

Comments
 (0)