commit 98ab2e05a75541639281fdbfa4a198e8152b272d Author: Andrew Miller Date: Wed Nov 5 15:25:20 2025 -0500 Initial Commit diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..b97f75f --- /dev/null +++ b/.eslintignore @@ -0,0 +1,8 @@ +node_modules/ +dist/ +dist-esm/ +coverage/ +*.js +*.mjs +*.d.ts +!jest.config.js diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..e73378f --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,25 @@ +{ + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 2020, + "sourceType": "module", + "project": "./tsconfig.json" + }, + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:@typescript-eslint/recommended-requiring-type-checking", + "prettier" + ], + "plugins": ["@typescript-eslint", "prettier"], + "rules": { + "prettier/prettier": "error", + "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }] + }, + "env": { + "node": true, + "es2020": true + } +} diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml new file mode 100644 index 0000000..fb11195 --- /dev/null +++ b/.gitea/workflows/build.yml @@ -0,0 +1,44 @@ +name: Build and Test + +on: + push: + branches: [ master, main, develop ] + pull_request: + branches: [ master, main, develop ] + +jobs: + build: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [16.x, 18.x, 20.x] + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run linter + run: npm run lint + + - name: Run tests + run: npm test + + - name: Build package + run: npm run build + + - name: Upload coverage + uses: actions/upload-artifact@v3 + if: matrix.node-version == '20.x' + with: + name: coverage + path: coverage/ diff --git a/.gitea/workflows/publish.yml b/.gitea/workflows/publish.yml new file mode 100644 index 0000000..b8e3f01 --- /dev/null +++ b/.gitea/workflows/publish.yml @@ -0,0 +1,33 @@ +name: Publish to NPM Registry + +on: + release: + types: [created] + +jobs: + publish: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: '20.x' + registry-url: 'https://git.dragonchain.com/api/packages/dragonchain/npm/' + + - name: Install dependencies + run: npm ci + + - name: Run tests + run: npm test + + - name: Build package + run: npm run build + + - name: Publish to Gitea NPM registry + run: npm publish + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c57161 --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +# Dependencies +node_modules/ +package-lock.json +yarn.lock + +# Build outputs +dist/ +dist-esm/ +*.tsbuildinfo + +# Test coverage +coverage/ +.nyc_output/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ +.DS_Store + +# Environment +.env +.env.local +.env.*.local + +# Logs +logs/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Temp files +*.tmp +.cache/ diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..59eb508 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,9 @@ +{ + "semi": true, + "trailingComma": "es5", + "singleQuote": true, + "printWidth": 100, + "tabWidth": 2, + "useTabs": false, + "arrowParens": "always" +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..80799c5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,190 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Support. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2025 Dragonchain + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b96b07e --- /dev/null +++ b/README.md @@ -0,0 +1,229 @@ +# Dragonchain Node.js SDK + +Official Node.js and TypeScript SDK for interacting with Dragonchain blockchain nodes. + +## Features + +- 🔒 **HMAC-SHA256 Authentication** - Secure request signing +- 📦 **Full TypeScript Support** - Complete type definitions included +- 🌐 **Dual Package** - Works with both ESM and CommonJS +- ✅ **Comprehensive Testing** - Thoroughly tested with Jest +- 🎯 **Modern Best Practices** - Built with latest Node.js standards + +## Installation + +```bash +npm install @dragonchain-inc/prime-sdk +``` + +Or with yarn: + +```bash +yarn add @dragonchain-inc/prime-sdk +``` + +## Quick Start + +```typescript +import { DragonchainSDK } from '@dragonchain-inc/prime-sdk'; + +// Initialize the SDK +const sdk = new DragonchainSDK( + 'your-public-id', + 'your-auth-key-id', + 'your-auth-key', + 'https://your-dragonchain-endpoint.com' +); + +// Check system health +await sdk.system.health(); + +// Get system status +const status = await sdk.system.status(); +console.log(`Chain ID: ${status.id}, Level: ${status.level}`); + +// Create a transaction +const txnResponse = await sdk.transaction.create({ + txn_type: 'my-transaction-type', + payload: JSON.stringify({ message: 'Hello Dragonchain' }), + tag: 'example-tag', +}); +console.log(`Created transaction: ${txnResponse.transaction_id}`); +``` + +## Using Configuration Files + +The SDK supports loading credentials from YAML configuration files: + +```yaml +default: my-chain-id +chains: + - name: my-chain + publicId: my-chain-id + authKeyId: my-auth-key-id + authKey: my-auth-key + endpoint: https://mychain.dragonchain.com +``` + +```typescript +import { DragonchainSDK } from '@dragonchain-inc/prime-sdk'; +import { loadConfig, getDefaultChain } from '@dragonchain-inc/prime-sdk'; + +// Load config from file +const config = loadConfig('~/.dragonchain/credentials.yaml'); +const chain = getDefaultChain(config); + +if (chain) { + const sdk = new DragonchainSDK( + chain.publicId, + chain.authKeyId, + chain.authKey, + chain.endpoint + ); +} +``` + +## API Reference + +### System + +```typescript +// Check system health +await sdk.system.health(); + +// Get system status +const status = await sdk.system.status(); +``` + +### Transactions + +```typescript +// Create a transaction +const response = await sdk.transaction.create({ + txn_type: 'my-type', + payload: JSON.stringify({ data: 'example' }), + tag: 'optional-tag', +}); + +// Create multiple transactions +const bulkResponse = await sdk.transaction.createBulk({ + transactions: [ + { txn_type: 'type1', payload: 'data1' }, + { txn_type: 'type2', payload: 'data2' }, + ], +}); + +// Get a transaction by ID +const txn = await sdk.transaction.get('transaction-id'); + +// List all transactions +const txns = await sdk.transaction.list(); +``` + +### Transaction Types + +```typescript +// Create a transaction type +await sdk.transactionType.create({ + txn_type: 'my-new-type', +}); + +// Get a transaction type +const txnType = await sdk.transactionType.get('my-type'); + +// List all transaction types +const types = await sdk.transactionType.list(); + +// Delete a transaction type +await sdk.transactionType.delete('my-type'); +``` + +### Blocks + +```typescript +// Get a block by ID +const block = await sdk.block.get('block-id'); +``` + +## TypeScript Support + +This SDK is written in TypeScript and includes complete type definitions: + +```typescript +import { + DragonchainSDK, + TransactionCreateRequest, + TransactionCreateResponse, + SystemStatus, +} from '@dragonchain-inc/prime-sdk'; + +const sdk = new DragonchainSDK( + publicId, + authKeyId, + authKey, + baseURL +); + +// Full type safety +const request: TransactionCreateRequest = { + txn_type: 'my-type', + payload: JSON.stringify({ foo: 'bar' }), +}; + +const response: TransactionCreateResponse = await sdk.transaction.create(request); +``` + +## Authentication + +The SDK uses HMAC-SHA256 authentication with the following components: + +1. **Public ID** - Your Dragonchain public identifier +2. **Auth Key ID** - Your authentication key identifier +3. **Auth Key** - Your secret authentication key +4. **Endpoint** - The base URL of your Dragonchain node + +Each request is signed with: +- `Authorization` header: `DC1-HMAC-SHA256 {authKeyId}:{signature}` +- `Dragonchain` header: Your public ID +- `Timestamp` header: Unix timestamp of the request + +## Development + +```bash +# Install dependencies +npm install + +# Run tests +npm test + +# Run tests with coverage +npm run test:coverage + +# Build the package +npm run build + +# Lint the code +npm run lint + +# Format the code +npm run format +``` + +## Requirements + +- Node.js >= 16.0.0 +- TypeScript >= 5.0.0 (for TypeScript projects) + +## License + +Apache-2.0 + +## Support + +For issues and questions, please visit: +- Repository: https://git.dragonchain.com/dragonchain/dragonchain-node-sdk +- Documentation: https://dragonchain-core-docs.dragonchain.com + +## Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. diff --git a/examples/basic-usage.ts b/examples/basic-usage.ts new file mode 100644 index 0000000..41aed1e --- /dev/null +++ b/examples/basic-usage.ts @@ -0,0 +1,185 @@ +// noinspection JSUnusedLocalSymbols + +/** + * Basic usage examples for Dragonchain Node.js SDK + */ + +import { DragonchainSDK, loadConfig, getDefaultChain } from '@dragonchain-inc/prime-sdk'; + +// Example 1: Initialize SDK with direct credentials +// @ts-ignore +async function directInitialization() { + const sdk = new DragonchainSDK( + 'your-public-id', + 'your-auth-key-id', + 'your-auth-key', + 'https://your-dragonchain-endpoint.com' + ); + + // Check system health + await sdk.system.health(); + console.log('System is healthy'); + + // Get system status + const status = await sdk.system.status(); + console.log(`Chain ID: ${status.id}, Level: ${status.level}`); +} + +// Example 2: Initialize SDK from config file +// @ts-ignore +async function configFileInitialization() { + // Load credentials from YAML file + const config = loadConfig('~/.dragonchain/credentials.yaml'); + const chain = getDefaultChain(config); + + if (!chain) { + throw new Error('No default chain found in config'); + } + + const sdk = new DragonchainSDK(chain.publicId, chain.authKeyId, chain.authKey, chain.endpoint); + + return sdk; +} + +// Example 3: Create a transaction +// @ts-ignore +async function createTransaction(sdk: DragonchainSDK) { + const response = await sdk.transaction.create({ + txn_type: 'my-transaction-type', + payload: JSON.stringify({ + message: 'Hello Dragonchain', + timestamp: new Date().toISOString(), + }), + tag: 'example-tag', + }); + + console.log(`Created transaction: ${response.transaction_id}`); + return response.transaction_id; +} + +// Example 4: Create bulk transactions +// @ts-ignore +async function createBulkTransactions(sdk: DragonchainSDK) { + const response = await sdk.transaction.createBulk({ + transactions: [ + { + txn_type: 'type1', + payload: JSON.stringify({ data: 'transaction 1' }), + }, + { + txn_type: 'type2', + payload: JSON.stringify({ data: 'transaction 2' }), + }, + { + txn_type: 'type3', + payload: JSON.stringify({ data: 'transaction 3' }), + }, + ], + }); + + console.log(`Created ${response.transaction_ids.length} transactions`); + return response.transaction_ids; +} + +// Example 5: Get a transaction +// @ts-ignore +async function getTransaction(sdk: DragonchainSDK, transactionId: string) { + const transaction = await sdk.transaction.get(transactionId); + console.log(`Transaction: ${transaction.header.txn_id}`); + console.log(`Type: ${transaction.header.txn_type}`); + console.log(`Block: ${transaction.header.block_id}`); + return transaction; +} + +// Example 6: Create and manage transaction types +// @ts-ignore +async function manageTransactionTypes(sdk: DragonchainSDK) { + // Create a new transaction type + await sdk.transactionType.create({ + txn_type: 'my-new-type', + }); + console.log('Transaction type created'); + + // Get the transaction type + const txnType = await sdk.transactionType.get('my-new-type'); + console.log(`Created at: ${txnType.created}`); + + // List all transaction types + const types = await sdk.transactionType.list(); + console.log(`Total transaction types: ${types.transactionTypes.length}`); +} + +// Example 7: Query blocks +// @ts-ignore +async function queryBlocks(sdk: DragonchainSDK, blockId: string) { + const block = await sdk.block.get(blockId); + console.log(`Block ID: ${block.block_id}`); + console.log(`Timestamp: ${block.timestamp}`); + console.log(`Transactions: ${block.transactions.length}`); + return block; +} + +// Example 8: Error handling +// @ts-ignore +async function errorHandlingExample(sdk: DragonchainSDK) { + try { + await sdk.transaction.get('non-existent-transaction-id'); + } catch (error) { + if (error instanceof Error) { + console.error(`Error: ${error.message}`); + // Handle specific error cases + if (error.message.includes('404')) { + console.log('Transaction not found'); + } + } + } +} + +// Example 9: Complete workflow +// @ts-ignore +async function completeWorkflow() { + // Initialize SDK + const sdk = new DragonchainSDK( + process.env.DC_PUBLIC_ID || '', + process.env.DC_AUTH_KEY_ID || '', + process.env.DC_AUTH_KEY || '', + process.env.DC_ENDPOINT || '' + ); + + try { + // Check system health + await sdk.system.health(); + console.log('✓ System healthy'); + + // Create a transaction type + await sdk.transactionType.create({ + txn_type: 'workflow-example', + }); + console.log('✓ Transaction type created'); + + // Create a transaction + const txnResponse = await sdk.transaction.create({ + txn_type: 'workflow-example', + payload: JSON.stringify({ + step: 'initialization', + timestamp: Date.now(), + }), + }); + console.log(`✓ Transaction created: ${txnResponse.transaction_id}`); + + // Wait a bit for the transaction to be processed + await new Promise((resolve) => setTimeout(resolve, 2000)); + + // Get the transaction + const transaction = await sdk.transaction.get(txnResponse.transaction_id); + console.log(`✓ Transaction verified in block: ${transaction.header.block_id}`); + + console.log('\nWorkflow completed successfully!'); + } catch (error) { + console.error('Workflow failed:', error); + throw error; + } +} + +// Run examples (uncomment to execute) +// completeWorkflow().catch(console.error); diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..9906040 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,19 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + roots: ['/tests'], + testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'], + transform: { + '^.+\\.ts$': 'ts-jest', + }, + collectCoverageFrom: [ + 'src/**/*.ts', + '!src/**/*.d.ts', + '!src/**/*.test.ts', + '!src/**/*.spec.ts', + ], + coverageDirectory: 'coverage', + coverageReporters: ['text', 'lcov', 'html'], + moduleFileExtensions: ['ts', 'js', 'json'], + verbose: true, +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..9ff4b4c --- /dev/null +++ b/package.json @@ -0,0 +1,68 @@ +{ + "name": "@dragonchain-inc/prime-sdk", + "version": "1.0.2", + "description": "Official Dragonchain SDK for Node.js and TypeScript", + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.js" + } + }, + "files": [ + "dist", + "src", + "README.md", + "LICENSE" + ], + "scripts": { + "build": "tsup", + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage", + "lint": "eslint src --ext .ts", + "lint:fix": "eslint src --ext .ts --fix", + "format": "prettier --write \"src/**/*.ts\" \"tests/**/*.ts\"", + "format:check": "prettier --check \"src/**/*.ts\" \"tests/**/*.ts\"", + "clean": "rm -rf dist coverage", + "prepublishOnly": "npm run clean && npm run build && npm test" + }, + "keywords": [ + "dragonchain", + "blockchain", + "sdk", + "api", + "typescript", + "nodejs" + ], + "author": "Dragonchain", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://git.dragonchain.com/dragonchain/dragonchain-node-sdk" + }, + "engines": { + "node": ">=16.0.0" + }, + "dependencies": { + "js-yaml": "^4.1.0" + }, + "devDependencies": { + "@types/jest": "^29.5.12", + "@types/js-yaml": "^4.0.9", + "@types/node": "^20.11.30", + "@typescript-eslint/eslint-plugin": "^7.3.1", + "@typescript-eslint/parser": "^7.3.1", + "eslint": "^8.57.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.1.3", + "jest": "^29.7.0", + "prettier": "^3.2.5", + "ts-jest": "^29.1.2", + "tsup": "^8.5.0", + "typescript": "^5.4.3" + } +} diff --git a/src/block.ts b/src/block.ts new file mode 100644 index 0000000..52d1cf4 --- /dev/null +++ b/src/block.ts @@ -0,0 +1,21 @@ +/** + * Block module for querying Dragonchain blocks + */ + +import { DragonchainClient } from './client'; +import { Block } from './types'; + +export class BlockClient { + private client: DragonchainClient; + + constructor(client: DragonchainClient) { + this.client = client; + } + + /** + * Gets a block by ID + */ + async get(blockId: string): Promise { + return this.client.get(`/api/v1/block/${blockId}`); + } +} diff --git a/src/client.ts b/src/client.ts new file mode 100644 index 0000000..3eb38b4 --- /dev/null +++ b/src/client.ts @@ -0,0 +1,207 @@ +/** + * HTTP Client with HMAC-SHA256 authentication for Dragonchain + */ + +import * as crypto from 'crypto'; +import * as https from 'https'; +import * as http from 'http'; +import { URL } from 'url'; + +export interface ClientConfig { + publicId: string; + authKeyId: string; + authKey: string; + baseURL: string; + timeout?: number; +} + +export class DragonchainClient { + private readonly publicId: string; + private readonly authKeyId: string; + private readonly authKey: string; + private readonly baseURL: string; + private readonly timeout: number; + + constructor(config: ClientConfig) { + this.publicId = config.publicId; + this.authKeyId = config.authKeyId; + this.authKey = config.authKey; + this.baseURL = config.baseURL.replace(/\/$/, ''); // Remove trailing slash + this.timeout = config.timeout || 30000; // Default 30 seconds + } + + /** + * Creates HMAC message string for signing + */ + private createHmacMessage( + method: string, + path: string, + timestamp: string, + contentType: string, + body: Buffer + ): string { + // Hash the body content + const contentHash = crypto.createHash('sha256').update(body).digest(); + const b64Content = contentHash.toString('base64'); + + // Format: METHOD\nPATH\nPUBLIC_ID\nTIMESTAMP\nCONTENT_TYPE\nBASE64_CONTENT_HASH + return [method.toUpperCase(), path, this.publicId, timestamp, contentType, b64Content].join( + '\n' + ); + } + + /** + * Creates HMAC signature + */ + private createHmac(message: string): string { + const hmac = crypto.createHmac('sha256', this.authKey); + hmac.update(message); + return hmac.digest('base64'); + } + + /** + * Generates the Authorization header + */ + private generateAuthHeader( + method: string, + path: string, + timestamp: string, + contentType: string, + body: Buffer + ): string { + const message = this.createHmacMessage(method, path, timestamp, contentType, body); + const signature = this.createHmac(message); + return `DC1-HMAC-SHA256 ${this.authKeyId}:${signature}`; + } + + /** + * Performs an HTTP request + */ + private async doRequest( + method: string, + path: string, + contentType: string, + body: unknown, + responseType?: 'json' | 'buffer' + ): Promise { + let bodyBuffer: Buffer; + + // Prepare request body + if (body === null || body === undefined) { + bodyBuffer = Buffer.from(''); + } else if (Buffer.isBuffer(body)) { + bodyBuffer = body; + } else if (typeof body === 'string') { + bodyBuffer = Buffer.from(body); + } else { + bodyBuffer = Buffer.from(JSON.stringify(body)); + if (!contentType) { + contentType = 'application/json'; + } + } + + // Generate authentication headers + const timestamp = Math.floor(Date.now() / 1000).toString(); + const authHeader = this.generateAuthHeader(method, path, timestamp, contentType, bodyBuffer); + + // Parse URL + const fullURL = `${this.baseURL}${path}`; + const parsedURL = new URL(fullURL); + const isHttps = parsedURL.protocol === 'https:'; + + // Prepare request options + const options: https.RequestOptions = { + hostname: parsedURL.hostname, + port: parsedURL.port || (isHttps ? 443 : 80), + path: parsedURL.pathname + parsedURL.search, + method: method.toUpperCase(), + headers: { + Authorization: authHeader, + Dragonchain: this.publicId, + Timestamp: timestamp, + ...(contentType && { 'Content-Type': contentType }), + 'Content-Length': bodyBuffer.length, + }, + timeout: this.timeout, + }; + + return new Promise((resolve, reject) => { + const httpModule = isHttps ? https : http; + const req = httpModule.request(options, (res) => { + const chunks: Buffer[] = []; + + res.on('data', (chunk: Buffer) => { + chunks.push(chunk); + }); + + res.on('end', () => { + const responseBody = Buffer.concat(chunks); + + // Check for errors + if (res.statusCode && res.statusCode >= 400) { + const errorMessage = responseBody.toString('utf8').trim(); + reject(new Error(`API error (status ${res.statusCode}): ${errorMessage}`)); + return; + } + + // Return response + if (responseType === 'buffer') { + resolve(responseBody as T); + } else if (responseBody.length > 0) { + try { + const parsed = JSON.parse(responseBody.toString('utf8')) as T; + resolve(parsed); + } catch (error) { + reject(new Error(`Failed to parse response: ${(error as Error).message}`)); + } + } else { + resolve({} as T); + } + }); + }); + + req.on('error', (error) => { + reject(new Error(`Request failed: ${error.message}`)); + }); + + req.on('timeout', () => { + req.destroy(); + reject(new Error(`Request timeout after ${this.timeout}ms`)); + }); + + // Write body and end request + if (bodyBuffer.length > 0) { + req.write(bodyBuffer); + } + req.end(); + }); + } + + /** + * Performs a GET request + */ + public async get(path: string): Promise { + return this.doRequest('GET', path, '', null); + } + + /** + * Performs a POST request + */ + public async post(path: string, contentType: string, body: unknown): Promise { + return this.doRequest('POST', path, contentType, body); + } + + /** + * Performs a PUT request + */ + public async put(path: string, contentType: string, body: unknown): Promise { + return this.doRequest('PUT', path, contentType, body); + } + + /** + * Performs a DELETE request + */ + public async delete(path: string): Promise { + return this.doRequest('DELETE', path, '', null); + } +} diff --git a/src/contract.ts b/src/contract.ts new file mode 100644 index 0000000..59ff4ad --- /dev/null +++ b/src/contract.ts @@ -0,0 +1,73 @@ +/** + * Contract module for managing Dragonchain smart contracts + */ + +import * as fs from 'fs'; +import { DragonchainClient } from './client'; +import { + ContentType, + SmartContractCreateRequest, + SmartContractUpdateRequest, + SmartContract, + ListResponse, + SuccessResponse, +} from './types'; + +export class ContractClient { + private client: DragonchainClient; + + constructor(client: DragonchainClient) { + this.client = client; + } + + /** + * Creates a new smart contract + */ + async create(request: SmartContractCreateRequest): Promise { + return this.client.post('/api/v1/contract', ContentType.JSON, request); + } + + /** + * Gets a smart contract by ID + */ + async get(contractId: string): Promise { + return this.client.get(`/api/v1/contract/${contractId}`); + } + + /** + * Lists all smart contracts + */ + async list(): Promise { + return this.client.get('/api/v1/contract'); + } + + /** + * Updates a smart contract + */ + async update(contractId: string, request: SmartContractUpdateRequest): Promise { + return this.client.put( + `/api/v1/contract/${contractId}`, + ContentType.JSON, + request + ); + } + + /** + * Uploads smart contract code + */ + async upload(contractId: string, filePath: string): Promise { + const fileContent = fs.readFileSync(filePath); + return this.client.put( + `/api/v1/contract/${contractId}/upload`, + ContentType.OCTET_STREAM, + fileContent + ); + } + + /** + * Deletes a smart contract + */ + async delete(contractId: string): Promise { + return this.client.delete(`/api/v1/contract/${contractId}`); + } +} diff --git a/src/credentials.ts b/src/credentials.ts new file mode 100644 index 0000000..c4fb1aa --- /dev/null +++ b/src/credentials.ts @@ -0,0 +1,109 @@ +/** + * Credentials module for loading and managing Dragonchain configuration + */ + +import * as fs from 'fs'; +import * as path from 'path'; +import * as yaml from 'js-yaml'; +import { homedir } from 'os'; + +/** + * Configuration for a single Dragonchain + */ +export interface ChainConfig { + name: string; + publicId: string; + authKeyId: string; + authKey: string; + endpoint: string; +} + +/** + * Complete configuration structure + */ +export interface Config { + default: string; + chains: ChainConfig[]; +} + +/** + * Expands file paths with home directory and environment variables + */ +function expandPath(filePath: string): string { + // Expand environment variables + let expanded = filePath.replace(/\$\{([^}]+)\}/g, (_, variable: string) => { + return process.env[variable] || ''; + }); + + // Handle tilde for home directory + if (expanded.startsWith('~/')) { + expanded = path.join(homedir(), expanded.slice(2)); + } else if (expanded === '~') { + expanded = homedir(); + } + + return expanded; +} + +/** + * Loads configuration from a YAML file + */ +export function loadConfig(filePath: string): Config { + const expandedPath = expandPath(filePath); + + if (!fs.existsSync(expandedPath)) { + throw new Error(`Config file not found: ${expandedPath}`); + } + + const fileContent = fs.readFileSync(expandedPath, 'utf8'); + const config = yaml.load(fileContent) as Config; + + if (!config.chains || !Array.isArray(config.chains)) { + throw new Error('Invalid config: missing or invalid chains array'); + } + + return config; +} + +/** + * Loads configuration from a YAML string + */ +export function loadConfigFromString(yamlContent: string): Config { + const config = yaml.load(yamlContent) as Config; + + if (!config.chains || !Array.isArray(config.chains)) { + throw new Error('Invalid config: missing or invalid chains array'); + } + + return config; +} + +/** + * Gets the default chain configuration + */ +export function getDefaultChain(config: Config): ChainConfig | undefined { + return config.chains.find((chain) => chain.publicId === config.default); +} + +/** + * Gets a chain configuration by public ID + */ +export function getChainByPublicId(config: Config, publicId: string): ChainConfig | undefined { + return config.chains.find((chain) => chain.publicId === publicId); +} + +/** + * Lists all chain names in the configuration + */ +export function listChains(config: Config): string[] { + return config.chains.map((chain) => chain.name); +} + +/** + * Saves configuration to a YAML file + */ +export function saveConfig(config: Config, filePath: string): void { + const expandedPath = expandPath(filePath); + const yamlContent = yaml.dump(config); + fs.writeFileSync(expandedPath, yamlContent, 'utf8'); +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..b67bb2f --- /dev/null +++ b/src/index.ts @@ -0,0 +1,77 @@ +/** + * Dragonchain SDK for Node.js and TypeScript + * + * Official SDK for interacting with Dragonchain blockchain nodes + */ + +import { DragonchainClient, ClientConfig } from './client'; +import { TransactionClient } from './transaction'; +import { TransactionTypeClient } from './transactionType'; +import { ContractClient } from './contract'; +import { BlockClient } from './block'; +import { SystemClient } from './system'; + +/** + * Main Dragonchain SDK class + */ +export class DragonchainSDK { + private client: DragonchainClient; + + public readonly transaction: TransactionClient; + public readonly transactionType: TransactionTypeClient; + public readonly contract: ContractClient; + public readonly block: BlockClient; + public readonly system: SystemClient; + + /** + * Creates a new Dragonchain SDK instance + * + * @param publicId - Your Dragonchain public ID + * @param authKeyId - Your authentication key ID + * @param authKey - Your authentication key + * @param baseURL - The base URL of your Dragonchain node + * @param timeout - Optional request timeout in milliseconds (default: 30000) + */ + constructor( + publicId: string, + authKeyId: string, + authKey: string, + baseURL: string, + timeout?: number + ) { + const config: ClientConfig = { + publicId, + authKeyId, + authKey, + baseURL, + timeout, + }; + + this.client = new DragonchainClient(config); + this.transaction = new TransactionClient(this.client); + this.transactionType = new TransactionTypeClient(this.client); + this.contract = new ContractClient(this.client); + this.block = new BlockClient(this.client); + this.system = new SystemClient(this.client); + } + + /** + * Gets the underlying HTTP client + */ + public getClient(): DragonchainClient { + return this.client; + } +} + +// Export all types and modules +export * from './types'; +export * from './credentials'; +export { DragonchainClient, ClientConfig } from './client'; +export { TransactionClient } from './transaction'; +export { TransactionTypeClient } from './transactionType'; +export { ContractClient } from './contract'; +export { BlockClient } from './block'; +export { SystemClient } from './system'; + +// Default export +export default DragonchainSDK; diff --git a/src/system.ts b/src/system.ts new file mode 100644 index 0000000..081bff6 --- /dev/null +++ b/src/system.ts @@ -0,0 +1,28 @@ +/** + * System module for Dragonchain system operations + */ + +import { DragonchainClient } from './client'; +import { SystemStatus } from './types'; + +export class SystemClient { + private client: DragonchainClient; + + constructor(client: DragonchainClient) { + this.client = client; + } + + /** + * Checks system health + */ + async health(): Promise { + await this.client.get('/api/v1/health'); + } + + /** + * Gets system status + */ + async status(): Promise { + return this.client.get('/api/v1/status'); + } +} diff --git a/src/transaction.ts b/src/transaction.ts new file mode 100644 index 0000000..7fa459c --- /dev/null +++ b/src/transaction.ts @@ -0,0 +1,58 @@ +/** + * Transaction module for managing Dragonchain transactions + */ + +import { DragonchainClient } from './client'; +import { + ContentType, + TransactionCreateRequest, + TransactionCreateResponse, + TransactionBulkRequest, + TransactionBulkResponse, + Transaction, + ListTransactionsResponse, +} from './types'; + +export class TransactionClient { + private client: DragonchainClient; + + constructor(client: DragonchainClient) { + this.client = client; + } + + /** + * Creates a new transaction + */ + async create(request: TransactionCreateRequest): Promise { + return this.client.post( + '/api/v1/transaction', + ContentType.JSON, + request + ); + } + + /** + * Creates multiple transactions in bulk + */ + async createBulk(request: TransactionBulkRequest): Promise { + return this.client.post( + '/api/v1/transaction/bulk', + ContentType.JSON, + request + ); + } + + /** + * Gets a transaction by ID + */ + async get(transactionId: string): Promise { + return this.client.get(`/api/v1/transaction/${transactionId}`); + } + + /** + * Lists all transactions + */ + async list(): Promise { + return this.client.get('/api/v1/transaction/'); + } +} diff --git a/src/transactionType.ts b/src/transactionType.ts new file mode 100644 index 0000000..4559c55 --- /dev/null +++ b/src/transactionType.ts @@ -0,0 +1,53 @@ +/** + * Transaction Type module for managing Dragonchain transaction types + */ + +import { DragonchainClient } from './client'; +import { + ContentType, + TransactionTypeCreateRequest, + TransactionTypeCreateResponse, + TransactionType, + TransactionListResponse, + SuccessResponse, +} from './types'; + +export class TransactionTypeClient { + private client: DragonchainClient; + + constructor(client: DragonchainClient) { + this.client = client; + } + + /** + * Creates a new transaction type + */ + async create(request: TransactionTypeCreateRequest): Promise { + return this.client.post( + '/api/v1/transaction-type', + ContentType.JSON, + request + ); + } + + /** + * Gets a transaction type by name + */ + async get(txnType: string): Promise { + return this.client.get(`/api/v1/transaction-type/${txnType}`); + } + + /** + * Lists all transaction types + */ + async list(): Promise { + return this.client.get('/api/v1/transaction-types'); + } + + /** + * Deletes a transaction type + */ + async delete(txnType: string): Promise { + return this.client.delete(`/api/v1/transaction-type/${txnType}`); + } +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..a9d5fee --- /dev/null +++ b/src/types.ts @@ -0,0 +1,162 @@ +/** + * Type definitions for Dragonchain SDK + */ + +export const ContentType = { + JSON: 'application/json', + OCTET_STREAM: 'application/octet-stream', +} as const; + +// Transaction Types +export interface TransactionCreateRequest { + version?: string; + txn_type: string; + payload: string | Record; + tag?: string; +} + +export interface TransactionCreateResponse { + transaction_id: string; +} + +export interface TransactionBulkRequest { + transactions: TransactionCreateRequest[]; +} + +export interface TransactionBulkResponse { + transaction_ids: string[]; +} + +export interface TransactionHeader { + tag: string; + dc_id: string; + txn_id: string; + invoker: string; + block_id: string; + txn_type: string; + timestamp: string; +} + +export interface TransactionProof { + full: string; + stripped: string; +} + +export interface Transaction { + version: string; + header: TransactionHeader; + proof: TransactionProof; + payload: string; +} + +export interface ListTransactionsResponse { + transactions: Transaction[]; +} + +// Transaction Type Types +export interface TransactionTypeCreateRequest { + version?: string; + txn_type: string; +} + +export interface TransactionTypeCreateResponse { + success: boolean; +} + +export interface TransactionType { + version: string; + created: number; + modified: number; + txn_type: string; + contract_id?: string; + custom_indexes: unknown[]; + active_since_block: string; +} + +export interface TransactionListResponse { + transactionTypes: TransactionType[]; +} + +// Smart Contract Types +export interface SmartContractCreateRequest { + environment: string; + transactionType: string; + executionOrder: string; + environmentVariables?: Record; + secrets?: Record; +} + +export interface SmartContractUpdateRequest { + version?: string; + enabled: boolean; + environmentVariables?: Record; + secrets?: Record; +} + +export interface SmartContractExecutionInfo { + type: string; + executablePath?: string; + executableWorkingDirectory?: string; + executableHash?: string; +} + +export interface SmartContract { + id: string; + created: number; + modified: number; + version: string; + environment: string; + transactionType: string; + executionOrder: string; + executionInfo?: SmartContractExecutionInfo; + envVars: Record; + secrets: string[]; +} + +// Block Types +export interface BlockProof { + scheme: string; + proof: string; + nonce?: number; +} + +export interface Block { + version: string; + block_id: string; + timestamp: string; + prev_id: string; + prev_proof: string; + transactions: string[]; + proof: BlockProof; +} + +// System Types +export interface SystemStatus { + id: string; + level: number; + url: string; + hashAlgo: string; + scheme: string; + version: string; + encryptionAlgo: string; + indexingEnabled: boolean; + // Level 5 Node specific fields + funded?: string; + broadcastInterval?: string; + network?: string; + interchainWallet?: string; +} + +// Generic Response Types +export interface SuccessResponse { + success: boolean; +} + +export interface ErrorResponse { + error: string; +} + +export interface ListResponse { + items: unknown[]; + total_count: number; +} diff --git a/tests/client.test.ts b/tests/client.test.ts new file mode 100644 index 0000000..b81f490 --- /dev/null +++ b/tests/client.test.ts @@ -0,0 +1,88 @@ +/** + * Tests for HTTP client with HMAC authentication + */ + +import * as crypto from 'crypto'; +import { DragonchainClient } from '../src/client'; + +describe('DragonchainClient', () => { + const testConfig = { + publicId: 'test-public-id', + authKeyId: 'test-auth-key-id', + authKey: 'test-auth-key', + baseURL: 'https://test.dragonchain.com', + }; + + let client: DragonchainClient; + + beforeEach(() => { + client = new DragonchainClient(testConfig); + }); + + describe('constructor', () => { + it('should create a client with correct configuration', () => { + expect(client).toBeDefined(); + expect(client.get).toBeDefined(); + }); + + it('should remove trailing slash from baseURL', () => { + const clientWithSlash = new DragonchainClient({ + ...testConfig, + baseURL: 'https://test.dragonchain.com/', + }); + expect(clientWithSlash).toBeDefined(); + }); + }); + + describe('HMAC authentication', () => { + it('should generate correct HMAC signature', () => { + // Test HMAC generation by creating a mock scenario + const method = 'POST'; + const path = '/api/v1/transaction'; + const timestamp = '1234567890'; + const contentType = 'application/json'; + const body = Buffer.from(JSON.stringify({ test: 'data' })); + + // Calculate expected values + const contentHash = crypto.createHash('sha256').update(body).digest('base64'); + const message = [ + method.toUpperCase(), + path, + testConfig.publicId, + timestamp, + contentType, + contentHash, + ].join('\n'); + + const expectedHmac = crypto + .createHmac('sha256', testConfig.authKey) + .update(message) + .digest('base64'); + + // The auth header should follow the format: DC1-HMAC-SHA256 {authKeyId}:{signature} + const expectedAuthHeader = `DC1-HMAC-SHA256 ${testConfig.authKeyId}:${expectedHmac}`; + + // We can't directly test the private method, but we verify the format is correct + expect(expectedAuthHeader).toContain('DC1-HMAC-SHA256'); + expect(expectedAuthHeader).toContain(testConfig.authKeyId); + }); + }); + + describe('request methods', () => { + it('should have get method', () => { + expect(typeof client.get).toBe('function'); + }); + + it('should have post method', () => { + expect(typeof client.post).toBe('function'); + }); + + it('should have put method', () => { + expect(typeof client.put).toBe('function'); + }); + + it('should have delete method', () => { + expect(typeof client.delete).toBe('function'); + }); + }); +}); diff --git a/tests/credentials.test.ts b/tests/credentials.test.ts new file mode 100644 index 0000000..d2c5b6f --- /dev/null +++ b/tests/credentials.test.ts @@ -0,0 +1,105 @@ +/** + * Tests for credentials module + */ + +import * as fs from 'fs'; +import * as path from 'path'; +import { loadConfig, loadConfigFromString, getDefaultChain, getChainByPublicId, listChains } from '../src/credentials'; + +describe('Credentials Module', () => { + const testYaml = ` +default: test-chain-id +chains: + - name: test-chain + publicId: test-chain-id + authKeyId: test-auth-key-id + authKey: test-auth-key + endpoint: https://test.dragonchain.com + - name: another-chain + publicId: another-chain-id + authKeyId: another-auth-key-id + authKey: another-auth-key + endpoint: https://another.dragonchain.com +`; + + describe('loadConfigFromString', () => { + it('should load config from YAML string', () => { + const config = loadConfigFromString(testYaml); + expect(config.default).toBe('test-chain-id'); + expect(config.chains).toHaveLength(2); + expect(config.chains[0].name).toBe('test-chain'); + }); + + it('should throw error for invalid YAML', () => { + expect(() => loadConfigFromString('invalid: [yaml')).toThrow(); + }); + + it('should throw error for missing chains array', () => { + expect(() => loadConfigFromString('default: test')).toThrow('Invalid config: missing or invalid chains array'); + }); + }); + + describe('loadConfig', () => { + const testConfigPath = path.join(__dirname, 'test-config.yaml'); + + beforeEach(() => { + fs.writeFileSync(testConfigPath, testYaml); + }); + + afterEach(() => { + if (fs.existsSync(testConfigPath)) { + fs.unlinkSync(testConfigPath); + } + }); + + it('should load config from file', () => { + const config = loadConfig(testConfigPath); + expect(config.default).toBe('test-chain-id'); + expect(config.chains).toHaveLength(2); + }); + + it('should throw error for non-existent file', () => { + expect(() => loadConfig('/non/existent/path.yaml')).toThrow('Config file not found'); + }); + }); + + describe('getDefaultChain', () => { + it('should return the default chain', () => { + const config = loadConfigFromString(testYaml); + const defaultChain = getDefaultChain(config); + expect(defaultChain).toBeDefined(); + expect(defaultChain?.name).toBe('test-chain'); + expect(defaultChain?.publicId).toBe('test-chain-id'); + }); + + it('should return undefined if default not found', () => { + const config = loadConfigFromString(testYaml); + config.default = 'non-existent'; + const defaultChain = getDefaultChain(config); + expect(defaultChain).toBeUndefined(); + }); + }); + + describe('getChainByPublicId', () => { + it('should return chain by public ID', () => { + const config = loadConfigFromString(testYaml); + const chain = getChainByPublicId(config, 'another-chain-id'); + expect(chain).toBeDefined(); + expect(chain?.name).toBe('another-chain'); + }); + + it('should return undefined if chain not found', () => { + const config = loadConfigFromString(testYaml); + const chain = getChainByPublicId(config, 'non-existent'); + expect(chain).toBeUndefined(); + }); + }); + + describe('listChains', () => { + it('should return all chain names', () => { + const config = loadConfigFromString(testYaml); + const names = listChains(config); + expect(names).toEqual(['test-chain', 'another-chain']); + }); + }); +}); diff --git a/tests/sdk.test.ts b/tests/sdk.test.ts new file mode 100644 index 0000000..da298d2 --- /dev/null +++ b/tests/sdk.test.ts @@ -0,0 +1,91 @@ +/** + * Tests for main SDK + */ + +import { DragonchainSDK } from '../src/index'; + +describe('DragonchainSDK', () => { + const testConfig = { + publicId: 'test-public-id', + authKeyId: 'test-auth-key-id', + authKey: 'test-auth-key', + baseURL: 'https://test.dragonchain.com', + }; + + let sdk: DragonchainSDK; + + beforeEach(() => { + sdk = new DragonchainSDK( + testConfig.publicId, + testConfig.authKeyId, + testConfig.authKey, + testConfig.baseURL + ); + }); + + describe('initialization', () => { + it('should create SDK instance', () => { + expect(sdk).toBeDefined(); + }); + + it('should initialize all client modules', () => { + expect(sdk.transaction).toBeDefined(); + expect(sdk.transactionType).toBeDefined(); + expect(sdk.contract).toBeDefined(); + expect(sdk.block).toBeDefined(); + expect(sdk.system).toBeDefined(); + }); + + it('should provide access to underlying client', () => { + const client = sdk.getClient(); + expect(client).toBeDefined(); + }); + }); + + describe('module methods', () => { + it('should have transaction module with correct methods', () => { + expect(typeof sdk.transaction.create).toBe('function'); + expect(typeof sdk.transaction.createBulk).toBe('function'); + expect(typeof sdk.transaction.get).toBe('function'); + expect(typeof sdk.transaction.list).toBe('function'); + }); + + it('should have transactionType module with correct methods', () => { + expect(typeof sdk.transactionType.create).toBe('function'); + expect(typeof sdk.transactionType.get).toBe('function'); + expect(typeof sdk.transactionType.list).toBe('function'); + expect(typeof sdk.transactionType.delete).toBe('function'); + }); + + it('should have contract module with correct methods', () => { + expect(typeof sdk.contract.create).toBe('function'); + expect(typeof sdk.contract.get).toBe('function'); + expect(typeof sdk.contract.list).toBe('function'); + expect(typeof sdk.contract.update).toBe('function'); + expect(typeof sdk.contract.upload).toBe('function'); + expect(typeof sdk.contract.delete).toBe('function'); + }); + + it('should have block module with correct methods', () => { + expect(typeof sdk.block.get).toBe('function'); + }); + + it('should have system module with correct methods', () => { + expect(typeof sdk.system.health).toBe('function'); + expect(typeof sdk.system.status).toBe('function'); + }); + }); + + describe('timeout configuration', () => { + it('should accept custom timeout', () => { + const customSdk = new DragonchainSDK( + testConfig.publicId, + testConfig.authKeyId, + testConfig.authKey, + testConfig.baseURL, + 60000 + ); + expect(customSdk).toBeDefined(); + }); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..7d60d13 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "outDir": "./dist", + "rootDir": "./src", + "declaration": true, + "sourceMap": true, + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "tests", "**/*.test.ts", "**/*.spec.ts"] +} diff --git a/tsup.config.ts b/tsup.config.ts new file mode 100644 index 0000000..f424baa --- /dev/null +++ b/tsup.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: ['src/index.ts'], + format: ['cjs', 'esm'], + dts: true, + splitting: false, + sourcemap: true, + clean: true, +});