Skip to content

Commit 18f8843

Browse files
committed
Add dynamic-codecs package
1 parent 62fcd21 commit 18f8843

22 files changed

+753
-0
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
module.exports = {
2+
extends: ['../../.eslintrc.js'],
3+
rules: {
4+
'sort-keys-fix/sort-keys-fix': 'off',
5+
},
6+
};

packages/dynamic-codecs/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
dist/
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
dist/
2+
e2e/
3+
test-ledger/
4+
target/
5+
CHANGELOG.md

packages/dynamic-codecs/LICENSE

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
MIT License
2+
3+
Copyright (c) 2024 Codama
4+
5+
Permission is hereby granted, free of charge, to any person obtaining
6+
a copy of this software and associated documentation files (the
7+
"Software"), to deal in the Software without restriction, including
8+
without limitation the rights to use, copy, modify, merge, publish,
9+
distribute, sublicense, and/or sell copies of the Software, and to
10+
permit persons to whom the Software is furnished to do so, subject to
11+
the following conditions:
12+
13+
The above copyright notice and this permission notice shall be
14+
included in all copies or substantial portions of the Software.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

packages/dynamic-codecs/README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Codama ➤ Dynamic Codecs
2+
3+
[![npm][npm-image]][npm-url]
4+
[![npm-downloads][npm-downloads-image]][npm-url]
5+
6+
[npm-downloads-image]: https://img.shields.io/npm/dm/@codama/dynamic-codecs.svg?style=flat
7+
[npm-image]: https://img.shields.io/npm/v/@codama/dynamic-codecs.svg?style=flat&label=%40codama%2Fdynamic-codecs
8+
[npm-url]: https://www.npmjs.com/package/@codama/dynamic-codecs
9+
10+
This package provides a set of helpers that provide `Codecs` for Codama nodes that describe data.
11+
12+
// TODO: Move this to `@codama/dynamic-parsers`
13+
This package provides a set of helpers that, given any Codama IDL, dynamically parses any byte array into deserialized accounts, instructions and defined types.
14+
15+
## Installation
16+
17+
```sh
18+
pnpm install @codama/dynamic-codecs
19+
```
20+
21+
> [!NOTE]
22+
> This package is **not** included in the main [`codama`](../library) package.
23+
24+
## Functions
25+
26+
TODO
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
{
2+
"name": "@codama/dynamic-codecs",
3+
"version": "1.0.0",
4+
"description": "Node specifications and helpers for the Codama standard",
5+
"exports": {
6+
"types": "./dist/types/index.d.ts",
7+
"react-native": "./dist/index.react-native.mjs",
8+
"browser": {
9+
"import": "./dist/index.browser.mjs",
10+
"require": "./dist/index.browser.cjs"
11+
},
12+
"node": {
13+
"import": "./dist/index.node.mjs",
14+
"require": "./dist/index.node.cjs"
15+
}
16+
},
17+
"browser": {
18+
"./dist/index.node.cjs": "./dist/index.browser.cjs",
19+
"./dist/index.node.mjs": "./dist/index.browser.mjs"
20+
},
21+
"main": "./dist/index.node.cjs",
22+
"module": "./dist/index.node.mjs",
23+
"react-native": "./dist/index.react-native.mjs",
24+
"types": "./dist/types/index.d.ts",
25+
"type": "commonjs",
26+
"files": [
27+
"./dist/types",
28+
"./dist/index.*"
29+
],
30+
"sideEffects": false,
31+
"keywords": [
32+
"solana",
33+
"framework",
34+
"standard",
35+
"specifications"
36+
],
37+
"scripts": {
38+
"build": "rimraf dist && pnpm build:src && pnpm build:types",
39+
"build:src": "zx ../../node_modules/@codama/internals/scripts/build-src.mjs package",
40+
"build:types": "zx ../../node_modules/@codama/internals/scripts/build-types.mjs",
41+
"dev": "zx ../../node_modules/@codama/internals/scripts/test-unit.mjs node --watch",
42+
"lint": "zx ../../node_modules/@codama/internals/scripts/lint.mjs",
43+
"lint:fix": "zx ../../node_modules/@codama/internals/scripts/lint.mjs --fix",
44+
"test": "pnpm test:types && pnpm test:treeshakability && pnpm test:browser && pnpm test:node && pnpm test:react-native",
45+
"test:browser": "zx ../../node_modules/@codama/internals/scripts/test-unit.mjs browser",
46+
"test:node": "zx ../../node_modules/@codama/internals/scripts/test-unit.mjs node",
47+
"test:react-native": "zx ../../node_modules/@codama/internals/scripts/test-unit.mjs react-native",
48+
"test:treeshakability": "zx ../../node_modules/@codama/internals/scripts/test-treeshakability.mjs",
49+
"test:types": "zx ../../node_modules/@codama/internals/scripts/test-types.mjs"
50+
},
51+
"dependencies": {
52+
"@codama/errors": "workspace:*",
53+
"@codama/nodes": "workspace:*",
54+
"@codama/visitors-core": "workspace:*",
55+
"@solana/addresses": "2.0.0-rc.3",
56+
"@solana/codecs": "2.0.0-rc.3"
57+
},
58+
"license": "MIT",
59+
"repository": {
60+
"type": "git",
61+
"url": "https://github.com/codama-idl/codama"
62+
},
63+
"bugs": {
64+
"url": "http://github.com/codama-idl/codama/issues"
65+
},
66+
"browserslist": [
67+
"supports bigint and not dead",
68+
"maintained node versions"
69+
]
70+
}
Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
import {
2+
AccountLinkNode,
3+
AccountNode,
4+
BytesEncoding,
5+
DefinedTypeLinkNode,
6+
DefinedTypeNode,
7+
InstructionArgumentLinkNode,
8+
InstructionArgumentNode,
9+
InstructionLinkNode,
10+
InstructionNode,
11+
isNode,
12+
NumberFormat,
13+
RegisteredTypeNode,
14+
structFieldTypeNodeFromInstructionArgumentNode,
15+
structTypeNodeFromInstructionArgumentNodes,
16+
} from '@codama/nodes';
17+
import {
18+
getLastNodeFromPath,
19+
getRecordLinkablesVisitor,
20+
LinkableDictionary,
21+
NodePath,
22+
NodeStack,
23+
pipe,
24+
recordNodeStackVisitor,
25+
visit,
26+
Visitor,
27+
} from '@codama/visitors-core';
28+
import { getAddressCodec } from '@solana/addresses';
29+
import {
30+
Codec,
31+
getArrayCodec,
32+
getBase16Codec,
33+
getBase58Codec,
34+
getBase64Codec,
35+
getBooleanCodec,
36+
getF32Codec,
37+
getF64Codec,
38+
getI8Codec,
39+
getI16Codec,
40+
getI32Codec,
41+
getI64Codec,
42+
getI128Codec,
43+
getShortU16Codec,
44+
getU8Codec,
45+
getU16Codec,
46+
getU32Codec,
47+
getU64Codec,
48+
getU128Codec,
49+
getUtf8Codec,
50+
NumberCodec,
51+
transformCodec,
52+
} from '@solana/codecs';
53+
54+
export type EncodableNodes =
55+
| AccountLinkNode
56+
| AccountNode
57+
| DefinedTypeLinkNode
58+
| DefinedTypeNode
59+
| InstructionArgumentLinkNode
60+
| InstructionArgumentNode
61+
| InstructionLinkNode
62+
| InstructionNode
63+
| RegisteredTypeNode;
64+
65+
type CodecOptions = {
66+
bytesEncoding?: BytesEncoding;
67+
};
68+
69+
export function getNodeCodec(path: NodePath<EncodableNodes>, options: CodecOptions = {}): Codec<unknown> {
70+
const linkables = new LinkableDictionary();
71+
visit(path[0], getRecordLinkablesVisitor(linkables));
72+
return visit(getLastNodeFromPath(path), getNodeCodecVisitor(linkables, options));
73+
}
74+
75+
export function getNodeCodecVisitor(
76+
linkables: LinkableDictionary,
77+
options: CodecOptions & { stack?: NodeStack } = {},
78+
): Visitor<Codec<unknown>, EncodableNodes['kind']> {
79+
const stack = options.stack ?? new NodeStack();
80+
const bytesEncoding = options.bytesEncoding ?? 'base64';
81+
82+
const errorHandler = (): Codec<unknown> => {
83+
throw new Error('Not implemented');
84+
};
85+
86+
const baseVisitor: Visitor<Codec<unknown>, EncodableNodes['kind']> = {
87+
visitAccount(node) {
88+
return visit(node.data, this);
89+
},
90+
visitAccountLink(node) {
91+
const path = linkables.getPathOrThrow(stack.getPath(node.kind));
92+
stack.pushPath(path);
93+
const result = visit(getLastNodeFromPath(path), this);
94+
stack.popPath();
95+
return result;
96+
},
97+
visitAmountType(node) {
98+
return visit(node.number, this);
99+
},
100+
visitArrayType(node) {
101+
const item = visit(node.item, this);
102+
if (isNode(node.count, 'prefixedCountNode')) {
103+
const size = visit(node.count.prefix, this) as NumberCodec;
104+
return getArrayCodec(item, { size }) as Codec<unknown>;
105+
} else if (isNode(node.count, 'fixedCountNode')) {
106+
return getArrayCodec(item, { size: node.count.value }) as Codec<unknown>;
107+
} else {
108+
return getArrayCodec(item, { size: 'remainder' }) as Codec<unknown>;
109+
}
110+
},
111+
visitBooleanType(node) {
112+
const size = visit(node.size, this) as NumberCodec;
113+
return getBooleanCodec({ size }) as Codec<unknown>;
114+
},
115+
visitBytesType() {
116+
return transformCodec(
117+
getCodecFromBytesEncoding(bytesEncoding),
118+
([, value]: [BytesEncoding, string]) => value,
119+
(value: string): [BytesEncoding, string] => [bytesEncoding, value],
120+
) as Codec<unknown>;
121+
},
122+
visitDateTimeType(node) {
123+
return visit(node.number, this);
124+
},
125+
visitDefinedType(node) {
126+
return visit(node.type, this);
127+
},
128+
visitDefinedTypeLink(node) {
129+
const path = linkables.getPathOrThrow(stack.getPath(node.kind));
130+
stack.pushPath(path);
131+
const result = visit(getLastNodeFromPath(path), this);
132+
stack.popPath();
133+
return result;
134+
},
135+
visitEnumType: errorHandler,
136+
visitEnumEmptyVariantType: errorHandler,
137+
visitEnumStructVariantType: errorHandler,
138+
visitEnumTupleVariantType: errorHandler,
139+
visitFixedSizeType: errorHandler,
140+
visitHiddenPrefixType: errorHandler,
141+
visitHiddenSuffixType: errorHandler,
142+
visitInstruction(node) {
143+
return visit(structTypeNodeFromInstructionArgumentNodes(node.arguments), this);
144+
},
145+
visitInstructionArgument(node) {
146+
return visit(structFieldTypeNodeFromInstructionArgumentNode(node), this);
147+
},
148+
visitInstructionArgumentLink(node) {
149+
const path = linkables.getPathOrThrow(stack.getPath(node.kind));
150+
stack.pushPath(path);
151+
const result = visit(getLastNodeFromPath(path), this);
152+
stack.popPath();
153+
return result;
154+
},
155+
visitInstructionLink(node) {
156+
const path = linkables.getPathOrThrow(stack.getPath(node.kind));
157+
stack.pushPath(path);
158+
const result = visit(getLastNodeFromPath(path), this);
159+
stack.popPath();
160+
return result;
161+
},
162+
visitMapType: errorHandler,
163+
visitNumberType(node) {
164+
return getCodecFromNumberFormat(node.format) as Codec<unknown>;
165+
},
166+
visitOptionType: errorHandler,
167+
visitPostOffsetType: errorHandler,
168+
visitPreOffsetType: errorHandler,
169+
visitPublicKeyType() {
170+
return getAddressCodec() as Codec<unknown>;
171+
},
172+
visitRemainderOptionType: errorHandler,
173+
visitSentinelType: errorHandler,
174+
visitSetType: errorHandler,
175+
visitSizePrefixType: errorHandler,
176+
visitSolAmountType(node) {
177+
return visit(node.number, this);
178+
},
179+
visitStringType(node) {
180+
return getCodecFromBytesEncoding(node.encoding) as Codec<unknown>;
181+
},
182+
visitStructType: errorHandler,
183+
visitStructFieldType: errorHandler,
184+
visitTupleType: errorHandler,
185+
visitZeroableOptionType: errorHandler,
186+
};
187+
188+
return pipe(baseVisitor, v => recordNodeStackVisitor(v, stack));
189+
}
190+
191+
function getCodecFromBytesEncoding(encoding: BytesEncoding) {
192+
switch (encoding) {
193+
case 'base16':
194+
return getBase16Codec();
195+
case 'base58':
196+
return getBase58Codec();
197+
case 'base64':
198+
return getBase64Codec();
199+
case 'utf8':
200+
return getUtf8Codec();
201+
default:
202+
// TODO: Coded error.
203+
throw new Error(`Unsupported bytes encoding: ${encoding satisfies never}`);
204+
}
205+
}
206+
207+
function getCodecFromNumberFormat(format: NumberFormat) {
208+
switch (format) {
209+
case 'u8':
210+
return getU8Codec();
211+
case 'u16':
212+
return getU16Codec();
213+
case 'u32':
214+
return getU32Codec();
215+
case 'u64':
216+
return getU64Codec();
217+
case 'u128':
218+
return getU128Codec();
219+
case 'i8':
220+
return getI8Codec();
221+
case 'i16':
222+
return getI16Codec();
223+
case 'i32':
224+
return getI32Codec();
225+
case 'i64':
226+
return getI64Codec();
227+
case 'i128':
228+
return getI128Codec();
229+
case 'f32':
230+
return getF32Codec();
231+
case 'f64':
232+
return getF64Codec();
233+
case 'shortU16':
234+
return getShortU16Codec();
235+
default:
236+
// TODO: Coded error.
237+
throw new Error(`Unsupported number format: ${format satisfies never}`);
238+
}
239+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './codecs';
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
declare const __BROWSER__: boolean;
2+
declare const __ESM__: boolean;
3+
declare const __NODEJS__: boolean;
4+
declare const __REACTNATIVE__: boolean;
5+
declare const __TEST__: boolean;
6+
declare const __VERSION__: string;
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { getBase16Encoder, ReadonlyUint8Array } from '@solana/codecs';
2+
3+
export function hex(hexadecimal: string): ReadonlyUint8Array {
4+
return getBase16Encoder().encode(hexadecimal);
5+
}

0 commit comments

Comments
 (0)