@cardananium/cquisitor-lib 0.1.0-beta.5 → 0.1.0-beta.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,530 @@
1
+ # Cquisitor-lib
2
+
3
+ A Cardano transaction validation and decoding library written in Rust and compiled to WebAssembly. Provides transaction validation according to ledger rules (Phase 1 and Phase 2), universal CBOR/Cardano type decoders, Plutus script decoders, and signature verification.
4
+
5
+ ## Features
6
+
7
+ ### Transaction Validation
8
+
9
+ Phase 1 validation covers balance, fees, witnesses, collateral, certificates, outputs, and transaction limits. Phase 2 executes Plutus V1/V2/V3 scripts with detailed redeemer results.
10
+
11
+ ### Universal Decoder
12
+
13
+ Decode 152+ Cardano types from hex/bech32/base58 encoding:
14
+ - Primitive types: `Address`, `PublicKey`, `PrivateKey`, `TransactionHash`, `ScriptHash`, etc.
15
+ - Complex structures: `Transaction`, `Block`, `TransactionBody`, `TransactionWitnessSet`
16
+ - Certificates: `StakeRegistration`, `PoolRegistration`, `DRepRegistration`, governance actions
17
+ - Plutus: `PlutusScript`, `PlutusData`, `Redeemer`, `ScriptRef`
18
+ - All credential types, native scripts, metadata structures
19
+
20
+ Functions:
21
+ - `get_decodable_types()` - Returns list of all supported type names
22
+ - `decode_specific_type(hex, type_name, params)` - Decode specific Cardano type
23
+ - `get_possible_types_for_input(hex)` - Suggests types that can decode given input
24
+
25
+ ### CBOR Decoder & CDDL Validation
26
+
27
+ - `cbor_to_json(cbor_hex)` - Converts raw CBOR to JSON with positional information, supporting indefinite arrays/maps and all CBOR types. Each node carries an optional `oddities` array flagging deviations from RFC 8949 deterministic encoding (overlong integers/floats, indefinite length, unsorted/duplicate map keys, non-canonical bignums). Never throws on malformed input — returns a `{ok, value}` / `{ok: false, error, partial?}` union where `error` is a structured `CborDecodeError` (kind / byte offset / byte span / semantic `path`) and `partial` is the sub-tree decoded before the failure, with every unfinished container flagged `incomplete: true`.
28
+ - `validate_cddl(cddl)` - Parses a CDDL schema; reports parse errors and unresolved rule references (e.g. `thing = [unknown_rule, int]` → `kind: "unresolved_references"`).
29
+ - `validate_cbor_against_cddl(cbor_hex, cddl, rule_name)` - Validates a CBOR payload against a named rule. Errors carry `kind`, `expected`, semantic `path`, byte/anchor spans, and an `additional` array when multiple violations fire.
30
+ - `decode_cbor_against_cddl(cbor_hex, cddl, rule_name)` - Maps decoded CBOR onto a CDDL schema and returns labelled JSON (e.g. Cardano `[body, witness_set, bool, aux]` becomes `{transaction_body, transaction_witness_set, ...}`). Handles generics (`set<a>`), tagged sets, type rules used as field labels, and a few well-known tags (bignum → string number, datetime → ISO string). Sub-structures the schema doesn't cover surface under `@extra` / `@positional` so partial matches don't lose data.
31
+
32
+ ### Plutus Script Decoder
33
+
34
+ - `decode_plutus_program_uplc_json(hex)` - Decodes Plutus script to UPLC AST JSON
35
+ - `decode_plutus_program_pretty_uplc(hex)` - Decodes to human-readable UPLC format
36
+
37
+ Handles double CBOR wrapping and normalization automatically.
38
+
39
+ ### Signature Verification
40
+
41
+ `check_block_or_tx_signatures(hex)` - Verifies all VKey and Catalyst witness signatures in transactions or entire blocks. Returns validation results with invalid signature details.
42
+
43
+ ### Script Execution
44
+
45
+ `execute_tx_scripts(tx_hex, utxos, cost_models)` - Executes all Plutus scripts in a transaction independently, returning execution units, logs, and success/failure for each redeemer.
46
+
47
+ ### Validation Coverage
48
+
49
+ **Phase 1 Validation:**
50
+ - Balance validation (inputs, outputs, fees, deposits, refunds)
51
+ - Fee calculation and validation (including script reference fees)
52
+ - Cryptographic witness validation (signatures, native scripts)
53
+ - Collateral validation for script transactions
54
+ - Certificate validation (stake registration, pool operations, DReps, governance)
55
+ - Output validation (minimum ADA, size limits)
56
+ - Transaction limits (size, execution units, reference scripts)
57
+ - Auxiliary data validation
58
+
59
+ **Phase 2 Validation:**
60
+ - Plutus V1, V2, and V3 script execution
61
+ - Redeemer validation with execution units
62
+ - Script context generation
63
+
64
+ See [WHAT-IS-COVERED.md](./WHAT-IS-COVERED.md) for a complete list of validation errors and warnings.
65
+
66
+ ## Installation
67
+
68
+ ### NPM/Yarn/PNPM
69
+
70
+ ```bash
71
+ npm install @cardananium/cquisitor-lib
72
+ ```
73
+
74
+ ```bash
75
+ yarn add @cardananium/cquisitor-lib
76
+ ```
77
+
78
+ ```bash
79
+ pnpm add @cardananium/cquisitor-lib
80
+ ```
81
+
82
+ ### Browser
83
+
84
+ For browser usage, import from the browser-specific build:
85
+
86
+ ```javascript
87
+ import { get_necessary_data_list_js, validate_transaction_js } from '@cardananium/cquisitor-lib/browser';
88
+ ```
89
+
90
+ ### Node.js
91
+
92
+ For Node.js usage:
93
+
94
+ ```javascript
95
+ import { get_necessary_data_list_js, validate_transaction_js } from '@cardananium/cquisitor-lib';
96
+ ```
97
+
98
+ ## Quick Start
99
+
100
+ ### Basic Usage
101
+
102
+ ```typescript
103
+ import {
104
+ get_necessary_data_list_js,
105
+ validate_transaction_js
106
+ } from '@cardananium/cquisitor-lib';
107
+
108
+ // Step 1: Parse transaction and identify required data
109
+ const txHex = "84a400..."; // Your transaction in hex format
110
+ const networkType = "mainnet"; // or "preview" | "preprod"
111
+ const necessaryDataJson = get_necessary_data_list_js(txHex, networkType);
112
+ const necessaryData = JSON.parse(necessaryDataJson);
113
+
114
+ console.log('Required UTXOs:', necessaryData.utxos);
115
+ console.log('Required accounts:', necessaryData.accounts);
116
+ console.log('Required pools:', necessaryData.pools);
117
+
118
+ // Step 2: Fetch the required data from your blockchain indexer
119
+ // (e.g., Blockfrost, Koios, or your own node)
120
+ const utxos = await fetchUtxos(necessaryData.utxos);
121
+ const accounts = await fetchAccounts(necessaryData.accounts);
122
+ const pools = await fetchPools(necessaryData.pools);
123
+ const protocolParams = await getProtocolParameters();
124
+ const currentSlot = await getCurrentSlot();
125
+
126
+ // Step 3: Build validation context
127
+ const validationContext = {
128
+ slot: currentSlot,
129
+ networkType: "mainnet", // or "preview" or "preprod"
130
+ protocolParameters: protocolParams,
131
+ utxoSet: utxos,
132
+ accountContexts: accounts,
133
+ poolContexts: pools,
134
+ drepContexts: [],
135
+ govActionContexts: [],
136
+ lastEnactedGovAction: [],
137
+ currentCommitteeMembers: [],
138
+ potentialCommitteeMembers: [],
139
+ treasuryValue: 0n
140
+ };
141
+
142
+ // Step 4: Validate the transaction
143
+ const resultJson = validate_transaction_js(
144
+ txHex,
145
+ JSON.stringify(validationContext)
146
+ );
147
+ const result = JSON.parse(resultJson);
148
+
149
+ // Step 5: Check validation results
150
+ if (result.errors.length > 0) {
151
+ console.error('❌ Transaction validation failed:');
152
+ result.errors.forEach(err => {
153
+ console.error(`- ${err.error_message}`);
154
+ if (err.hint) {
155
+ console.error(` Hint: ${err.hint}`);
156
+ }
157
+ });
158
+ } else if (result.phase2_errors.length > 0) {
159
+ console.error('❌ Script execution failed:');
160
+ result.phase2_errors.forEach(err => {
161
+ console.error(`- ${err.error_message}`);
162
+ });
163
+ } else {
164
+ console.log('✅ Transaction is valid!');
165
+ }
166
+
167
+ // Check for warnings
168
+ if (result.warnings.length > 0) {
169
+ console.warn('⚠️ Warnings:', result.warnings);
170
+ }
171
+ ```
172
+
173
+ ### Complete Example with Error Handling
174
+
175
+ ```typescript
176
+ import {
177
+ get_necessary_data_list_js,
178
+ validate_transaction_js
179
+ } from '@cardananium/cquisitor-lib';
180
+
181
+ async function validateTransaction(txHex: string): Promise<boolean> {
182
+ try {
183
+ // Parse transaction
184
+ const necessaryDataJson = get_necessary_data_list_js(txHex, "mainnet");
185
+ const necessaryData = JSON.parse(necessaryDataJson);
186
+
187
+ // Fetch required blockchain data
188
+ // (Implementation depends on your data source)
189
+ const context = await buildValidationContext(necessaryData);
190
+
191
+ // Validate
192
+ const resultJson = validate_transaction_js(
193
+ txHex,
194
+ JSON.stringify(context)
195
+ );
196
+ const result = JSON.parse(resultJson);
197
+
198
+ // Log detailed results
199
+ const hasErrors = result.errors.length > 0 || result.phase2_errors.length > 0;
200
+
201
+ if (!hasErrors) {
202
+ console.log('✅ Transaction is valid!');
203
+
204
+ // Log redeemer execution details
205
+ result.eval_redeemer_results.forEach(redeemer => {
206
+ console.log(`Redeemer ${redeemer.tag}[${redeemer.index}]:`);
207
+ console.log(` Success: ${redeemer.success}`);
208
+ console.log(` Ex units: ${JSON.stringify(redeemer.calculated_ex_units)}`);
209
+ if (redeemer.logs.length > 0) {
210
+ console.log(` Logs: ${redeemer.logs.join(', ')}`);
211
+ }
212
+ });
213
+ } else {
214
+ console.error('❌ Validation failed');
215
+ [...result.errors, ...result.phase2_errors].forEach(err => {
216
+ console.error(`- ${err.error_message}`);
217
+ });
218
+ }
219
+
220
+ return !hasErrors;
221
+
222
+ } catch (error) {
223
+ console.error('Validation error:', error);
224
+ return false;
225
+ }
226
+ }
227
+ ```
228
+
229
+ ## API Reference
230
+
231
+ ### Transaction Validation
232
+
233
+ #### `get_necessary_data_list_js(tx_hex: string, network_type: "mainnet" | "preview" | "preprod"): string`
234
+
235
+ Extracts required blockchain data for validation. `network_type` determines the bech32 prefix used when deriving stake/reward addresses for `accounts`, `pools`, and `dReps`.
236
+
237
+ ```typescript
238
+ const necessaryData = JSON.parse(get_necessary_data_list_js(txHex, "mainnet"));
239
+ // Returns: { utxos, accounts, pools, dReps, govActions, ... }
240
+ ```
241
+
242
+ #### `validate_transaction_js(tx_hex: string, validation_context: string): string`
243
+
244
+ Validates transaction with full ledger rules.
245
+
246
+ ```typescript
247
+ const result = JSON.parse(validate_transaction_js(txHex, JSON.stringify(context)));
248
+ // Returns: { errors, warnings, phase2_errors, phase2_warnings, eval_redeemer_results }
249
+ ```
250
+
251
+ #### `get_utxo_list_from_tx(tx_hex: string): string[]`
252
+
253
+ Extracts all UTxO references (inputs + collateral + reference inputs) from transaction.
254
+
255
+ #### `get_ref_script_bytes(tx_hex: string, output_index: number): string`
256
+
257
+ Returns the hex-encoded CBOR bytes of the reference script embedded in `outputs[output_index]`. Returns an empty string if the output has no reference script or the index is out of range.
258
+
259
+ ```typescript
260
+ const scriptHex = get_ref_script_bytes(txHex, 0);
261
+ ```
262
+
263
+ #### `extract_hashes_from_transaction_js(tx_hex: string): string`
264
+
265
+ Returns a JSON-serialized `ExtractedHashes` with every script / datum / redeemer / metadata / auxiliary-data hash referenced by the transaction (witness set, outputs with inline scripts/datums, auxiliary data). Useful for building indexers or caches.
266
+
267
+ ```typescript
268
+ const hashes = JSON.parse(extract_hashes_from_transaction_js(txHex));
269
+ // { witness_native_script_hashes, witness_plutus_scripts, witness_datum_hashes, ... }
270
+ ```
271
+
272
+ ### Universal Decoder
273
+
274
+ #### `get_decodable_types(): string[]`
275
+
276
+ Returns array of all 152+ decodable type names.
277
+
278
+ ```typescript
279
+ const types = get_decodable_types();
280
+ // ['Address', 'Transaction', 'PlutusScript', 'PublicKey', ...]
281
+ ```
282
+
283
+ #### `decode_specific_type(input: string, type_name: string, params: DecodingParams): any`
284
+
285
+ Decodes specific Cardano type from hex/bech32/base58.
286
+
287
+ ```typescript
288
+ const address = decode_specific_type(
289
+ "addr1...",
290
+ "Address",
291
+ { plutusDataSchema: "DetailedSchema" }
292
+ );
293
+
294
+ const tx = decode_specific_type(
295
+ "84a400...",
296
+ "Transaction",
297
+ { plutusDataSchema: "DetailedSchema" }
298
+ );
299
+ ```
300
+
301
+ #### `get_possible_types_for_input(input: string): string[]`
302
+
303
+ Suggests which types can decode the given input.
304
+
305
+ ```typescript
306
+ const possibleTypes = get_possible_types_for_input("e1a...");
307
+ // ['Address', 'BaseAddress', 'EnterpriseAddress', ...]
308
+ ```
309
+
310
+ ### CBOR Decoder
311
+
312
+ #### `cbor_to_json(cbor_hex: string): CborDecodeResult`
313
+
314
+ Converts CBOR to JSON with positional metadata. Each node has `position_info` (byte span of its header) and, for containers/tags, `struct_position_info` (span of the whole subtree). Non-canonical encoding deviations (per RFC 8949 §4.1/§4.2) are flagged locally on the offending node via an optional `oddities: CborOddity[]` field — canonical inputs omit the field entirely.
315
+
316
+ The function **never throws** on malformed input. On success it returns `{ ok: true, value }`; on failure `{ ok: false, error, partial? }` where `error` is a structured `CborDecodeError` and `partial` is the sub-tree decoded up to the failure point:
317
+
318
+ ```typescript
319
+ const r = cbor_to_json("a26461646472...");
320
+ if (r.ok) {
321
+ // r.value — the full positional tree; each node may carry oddities like:
322
+ // { kind: "IntNotShortest", detail: "value 15 uses 2-byte header, shortest is 1" }
323
+ // { kind: "IndefiniteLength", detail: "indefinite-length map" }
324
+ // { kind: "MapKeysNotSorted", detail: "key at offset 3 sorts after key at offset 5" }
325
+ // { kind: "DuplicateMapKeys", detail: "duplicate key at offsets 3 and 6" }
326
+ // { kind: "BignumForSmallInt", detail: "unsigned bignum fits in a native CBOR integer" }
327
+ } else {
328
+ // r.error: { kind, offset?, byte_span?, path, message }
329
+ // kind — machine-readable tag ("invalid_syntax", "unexpected_eof", ...).
330
+ // offset — byte where decoding stopped.
331
+ // byte_span — { offset, length } when the failure pins a range.
332
+ // path — structural location, e.g. "$.entries[1].value[0]".
333
+ // r.partial (optional) — same shape as a CborValue, but every unfinished
334
+ // container carries `incomplete: true`, and partial map entries carry
335
+ // `incomplete_at: "key" | "value"` on the half that didn't parse.
336
+ }
337
+ ```
338
+
339
+ See `CborOddityKind` and `CborDecodeErrorKind` in the type definitions for the full lists.
340
+
341
+ #### `validate_cddl(cddl: string): { valid: boolean, error?: object }`
342
+
343
+ Parses a CDDL schema and reports whether it is well-formed. Beyond surface parse errors this also catches **dangling rule references** at parse time, surfaced as `kind: "unresolved_references"`.
344
+
345
+ ```typescript
346
+ validate_cddl("thing = {n: uint}");
347
+ // { valid: true }
348
+
349
+ validate_cddl("thing = [unknown_rule, int]");
350
+ // { valid: false, error: { kind: "unresolved_references",
351
+ // message: "missing definition for rule unknown_rule" } }
352
+ ```
353
+
354
+ `error.kind` values: `"parse_error"`, `"unresolved_references"`.
355
+
356
+ #### `validate_cbor_against_cddl(cbor_hex: string, cddl: string, rule_name: string): { valid: boolean, error?: object }`
357
+
358
+ Validates a CBOR payload against a specific rule in a CDDL schema. The rule does not have to be the first rule in the document — when it isn't, the validator wraps it in a synthetic root internally.
359
+
360
+ ```typescript
361
+ validate_cbor_against_cddl("01", "thing = tstr", "thing");
362
+ // {
363
+ // valid: false,
364
+ // error: {
365
+ // kind: "mismatch",
366
+ // expected: "tstr",
367
+ // path: "$",
368
+ // byte_spans: [{ offset: 0, length: 1 }],
369
+ // message: "expected type tstr, got Integer(Integer(1))"
370
+ // }
371
+ // }
372
+ ```
373
+
374
+ `error.kind` values: `"parse_error"`, `"unresolved_references"`, `"missing_rule"`, `"input_parse"`, `"mismatch"`, `"map_cut"`, `"generic"`. When multiple violations fire, the headline goes in the top-level fields and the rest land in `error.additional`.
375
+
376
+ #### `decode_cbor_against_cddl(cbor_hex: string, cddl: string, rule_name: string): unknown`
377
+
378
+ Walks the CDDL alongside the decoded CBOR and produces a JSON tree where positional/numeric-keyed structures are replaced with the names the schema declares. Useful for turning a Cardano transaction CBOR into something inspectable without hand-mapping every field.
379
+
380
+ ```typescript
381
+ decode_cbor_against_cddl(txHex, conwayCddl, "transaction");
382
+ // {
383
+ // transaction_body: {
384
+ // 0: { "@tag": 258, "@value": [{ transaction_id: "16b6...", index: 0 }] },
385
+ // 1: [{ address: "00ae...", amount: 1_000_000 }, ...],
386
+ // 2: 200000,
387
+ // 7: "bdaa..."
388
+ // },
389
+ // transaction_witness_set: {
390
+ // 0: { "@tag": 258, "@value": [{ vkey: "f8f5...", signature: "1e14..." }] }
391
+ // },
392
+ // "@positional": [true, { "@tag": 259, "@value": {} }]
393
+ // }
394
+ ```
395
+
396
+ Recognised features: type choices (first match wins), generics (`set<a>`), tagged data (well-known tags 0/2/3 specialised to ISO date / bignum string), rule references, optionals/repetitions, prelude scalars. Sub-structures the schema doesn't cover or that don't match any choice fall back to a raw form under `@extra` (maps) or `@positional` (arrays) so data is never silently dropped.
397
+
398
+ ### Plutus Script Decoder
399
+
400
+ #### `decode_plutus_program_uplc_json(hex: string): ProgramJson`
401
+
402
+ Decodes Plutus script to UPLC AST in JSON format.
403
+
404
+ ```typescript
405
+ const program = decode_plutus_program_uplc_json("59012a01000...");
406
+ // Returns: { version: [1,0,0], program: { ... } }
407
+ ```
408
+
409
+ #### `decode_plutus_program_pretty_uplc(hex: string): string`
410
+
411
+ Decodes Plutus script to human-readable UPLC.
412
+
413
+ ```typescript
414
+ const code = decode_plutus_program_pretty_uplc("59012a01000...");
415
+ // Returns: "(program 1.0.0 (lam x_0 ...))"
416
+ ```
417
+
418
+ ### Signature Verification
419
+
420
+ #### `check_block_or_tx_signatures(hex: string): CheckSignaturesResult`
421
+
422
+ Verifies all signatures in transaction or block.
423
+
424
+ ```typescript
425
+ const result = check_block_or_tx_signatures(txHex);
426
+ // Returns: { valid, results: [{ valid, tx_hash, invalidVkeyWitnesses, invalidCatalystWitnesses }] }
427
+ ```
428
+
429
+ ### Script Execution
430
+
431
+ #### `execute_tx_scripts(tx_hex: string, utxos: UTxO[], cost_models: CostModels): ExecuteTxScriptsResult`
432
+
433
+ Executes all Plutus scripts in transaction.
434
+
435
+ ```typescript
436
+ const result = execute_tx_scripts(txHex, utxos, costModels);
437
+ // Returns execution units, logs, and status for each redeemer
438
+ ```
439
+
440
+ ## Data Sources
441
+
442
+ To populate the validation context, you'll need to fetch blockchain data from a Cardano indexer or node. Recommended sources:
443
+
444
+ - **[Blockfrost](https://blockfrost.io/)** - Reliable API with generous free tier
445
+ - **[Koios](https://koios.rest/)** - Community-driven API with rich queries
446
+ - **Cardano Node** - Direct access via `cardano-cli` or `cardano-db-sync`
447
+ - **Custom Indexer** - Roll your own using Pallas or similar libraries
448
+
449
+ ## Building from Source
450
+
451
+ ### Prerequisites
452
+
453
+ - Rust 1.83 or newer
454
+ - `wasm-pack`
455
+ - Node.js and npm
456
+
457
+ ### Build Steps
458
+
459
+ ```bash
460
+ # Clone the repository
461
+ git clone https://github.com/your-org/cquisitor-lib.git
462
+ cd cquisitor-lib
463
+
464
+ # Build for Node.js
465
+ npm run rust:build-wasm:node
466
+
467
+ # Build for browser
468
+ npm run rust:build-wasm:browser
469
+
470
+ # Build both targets
471
+ npm run build-all
472
+
473
+ # Generate TypeScript definitions
474
+ npm run generate-dts
475
+ ```
476
+
477
+ ## Type Definitions
478
+
479
+ Full TypeScript type definitions are available in the package and cover all input and output types. The main types include:
480
+
481
+ - `NecessaryInputData` - Required blockchain data for validation
482
+ - `ValidationInputContext` - Complete validation context structure
483
+ - `ValidationResult` - Validation results with errors and warnings
484
+ - `ProtocolParameters` - Cardano protocol parameters
485
+ - And many more detailed types for UTXOs, certificates, governance, etc.
486
+
487
+ See [types/cquisitor_lib.d.ts](./types/cquisitor_lib.d.ts) for the complete type definitions.
488
+
489
+ ## Performance
490
+
491
+ Written in Rust and compiled to WebAssembly for near-native performance in browsers and Node.js.
492
+
493
+ ## Contributing
494
+
495
+ Contributions are welcome! Please feel free to submit pull requests or open issues for bugs and feature requests.
496
+
497
+ ### Development Workflow
498
+
499
+ 1. Fork the repository
500
+ 2. Create a feature branch (`git checkout -b feature/amazing-feature`)
501
+ 3. Make your changes
502
+ 4. Run tests (`cargo test`)
503
+ 5. Commit your changes (`git commit -m 'Add amazing feature'`)
504
+ 6. Push to the branch (`git push origin feature/amazing-feature`)
505
+ 7. Open a Pull Request
506
+
507
+ ## License
508
+
509
+ This project is licensed under the Apache License 2.0 - see the [LICENSE](./LICENSE) file for details.
510
+
511
+ ## Acknowledgments
512
+
513
+ This library builds upon the excellent work of the Cardano community, particularly:
514
+
515
+ - [cardano-serialization-lib](https://github.com/Emurgo/cardano-serialization-lib) - For cardano structures deserialization
516
+ - [Pallas](https://github.com/txpipe/pallas) - Cardano primitives
517
+ - [UPLC](https://github.com/aiken-lang/aiken/tree/main/crates/uplc) - Plutus script execution
518
+ - The Cardano Ledger specification team
519
+
520
+ ## Support
521
+
522
+ For questions and support:
523
+
524
+ - 📖 Check the [API Documentation](./API_DOCUMENTATION.md)
525
+ - 🐛 Report bugs via [GitHub Issues](https://github.com/cardananium/cquisitor-lib/issues)
526
+
527
+ ---
528
+
529
+ Made with ❤️ for the Cardano ecosystem
530
+