diff --git a/CODEOWNERS b/CODEOWNERS index 4305da573d..abbead3339 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -135,6 +135,7 @@ /modules/statics/src/utxo.ts @BitGo/btc-team /modules/web-demo/ @BitGo/coins @BitGo/web-experience @BitGo/wallet-core @BitGo/wallet-core-india /scripts/ @BitGo/coins @BitGo/web-experience @BitGo/wallet-core @BitGo/wallet-core-india @BitGo/developer-experience +/scripts/sdk-coin-generator-v2/ @BitGo/ethalt-team /types/ @BitGo/coins @BitGo/web-experience @BitGo/wallet-core @BitGo/wallet-core-india /webpack/ @BitGo/coins @BitGo/web-experience @BitGo/wallet-core @BitGo/wallet-core-india diff --git a/package.json b/package.json index 0cad1c56e8..5b53d046bf 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "devDependencies": { "@babel/core": "^7.28.0", "@babel/preset-env": "^7.28.0", + "@clack/prompts": "^0.7.0", "@commitlint/cli": "^19.4.0", "@commitlint/config-conventional": "^19.2.2", "@types/lodash": "^4.14.151", @@ -141,6 +142,7 @@ "dev": "tsc -b ./tsconfig.packages.json -w", "prepare": "husky install", "sdk-coin:new": "yo ./scripts/sdk-coin-generator", + "sdk-coin:new-v2": "tsx ./scripts/sdk-coin-generator-v2/index.ts", "build-docker-express": "yarn update-dockerfile && ./scripts/build-docker-express.sh", "update-dockerfile": "tsx scripts/update-dockerfile.ts", "precommit": "lint-staged", diff --git a/scripts/sdk-coin-generator-v2/PLUGIN_DEVELOPMENT.md b/scripts/sdk-coin-generator-v2/PLUGIN_DEVELOPMENT.md new file mode 100644 index 0000000000..8f1d1d7f59 --- /dev/null +++ b/scripts/sdk-coin-generator-v2/PLUGIN_DEVELOPMENT.md @@ -0,0 +1,431 @@ +# Plugin Development Guide + +This guide explains how to add a new chain type plugin to the SDK coin generator. + +## Overview + +The generator uses a plugin architecture where each chain type (generic-l1, evm-like, substrate-like, cosmos) is implemented as a plugin. Adding a new chain type is as simple as creating a plugin class and registering it. + +## Quick Example + +```typescript +// plugins/my-chain.ts +import { BaseChainPlugin } from './base'; +import type { CoinConfig, DependencySet } from '../core/types'; + +export class MyChainPlugin extends BaseChainPlugin { + readonly id = 'my-chain'; + readonly name = 'My Chain'; + readonly description = 'My custom chain type'; + readonly examples = ['Example1', 'Example2']; + + async getDependencies(config: CoinConfig, contextRoot: string): Promise { + return { + dependencies: { + ...(await this.getBaseDependencies(contextRoot)), + ...(await this.getTssDependencies(config, contextRoot)), + '@bitgo/abstract-mychain': await this.getVersionFromPackage('abstract-mychain', contextRoot), + }, + devDependencies: await this.getBaseDevDependencies(contextRoot), + }; + } +} +``` + +Then register in `plugins/index.ts`: + +```typescript +import { MyChainPlugin } from './my-chain'; + +private registerDefaultPlugins(): void { + this.register(new GenericL1Plugin()); + this.register(new EvmLikePlugin()); + this.register(new SubstrateLikePlugin()); + this.register(new CosmosPlugin()); + this.register(new MyChainPlugin()); // Add here +} + +export { MyChainPlugin }; // Export for direct access +``` + +That's it! Your new chain type will appear in the prompts. + +## Step-by-Step Guide + +### 1. Create Plugin File + +Create `plugins/my-chain.ts`: + +```typescript +import { BaseChainPlugin } from './base'; +import type { CoinConfig, DependencySet } from '../core/types'; + +export class MyChainPlugin extends BaseChainPlugin { + // Unique ID used internally (lowercase-with-dashes) + readonly id = 'my-chain'; + + // Display name shown in prompts + readonly name = 'My Chain'; + + // Description/hint shown in prompts + readonly description = 'My custom chain type description'; + + // Examples shown to users + readonly examples = ['Example Coin 1', 'Example Coin 2']; + + // Required: Define dependencies for this chain type + async getDependencies(config: CoinConfig, contextRoot: string): Promise { + const dependencies = { + // Always include base dependencies + ...(await this.getBaseDependencies(contextRoot)), + + // Add TSS dependencies if enabled + ...(await this.getTssDependencies(config, contextRoot)), + + // Add chain-specific dependencies + '@bitgo/abstract-mychain': await this.getVersionFromPackage('abstract-mychain', contextRoot), + 'mychain-sdk': '^1.0.0', // External dependencies + }; + + const devDependencies = await this.getBaseDevDependencies(contextRoot); + + return { dependencies, devDependencies }; + } +} +``` + +### 2. Register Plugin + +Update `plugins/index.ts`: + +```typescript +import { MyChainPlugin } from './my-chain'; + +export class PluginRegistry { + private registerDefaultPlugins(): void { + this.register(new GenericL1Plugin()); + this.register(new EvmLikePlugin()); + this.register(new SubstrateLikePlugin()); + this.register(new CosmosPlugin()); + this.register(new MyChainPlugin()); // Add your plugin + } +} + +// Export for direct access if needed +export { GenericL1Plugin, EvmLikePlugin, SubstrateLikePlugin, CosmosPlugin, MyChainPlugin }; +``` + +### 3. Test Your Plugin + +```bash +yarn sdk-coin:new-v2 +``` + +Your new chain type will appear in the list! + +## Plugin Methods + +### Required Methods + +#### `getDependencies(config, contextRoot)` +Returns dependencies for this chain type. + +```typescript +async getDependencies(config: CoinConfig, contextRoot: string): Promise { + return { + dependencies: { + ...(await this.getBaseDependencies(contextRoot)), + // Add your dependencies + }, + devDependencies: await this.getBaseDevDependencies(contextRoot), + }; +} +``` + +### Optional Methods + +#### `getTemplateDir()` +Specify which template directory to use (defaults to `id`). + +```typescript +getTemplateDir(): string { + // Use generic-l1 templates until you create custom ones + return 'generic-l1'; + + // Or use custom templates + // return 'my-chain'; // Must create templates/my-chain/ +} +``` + +#### `validate(config)` +Add custom validation logic. + +```typescript +validate(config: CoinConfig): ValidationResult { + // Call base validation first + const baseResult = super.validate(config); + + // Add custom validation + const errors: string[] = [...(baseResult.errors || [])]; + const warnings: string[] = [...(baseResult.warnings || [])]; + + if (config.symbol.length < 3) { + errors.push('Symbol must be at least 3 characters for My Chain'); + } + + if (config.baseFactor !== '1e18') { + warnings.push('My Chain typically uses 1e18 as base factor'); + } + + return { + valid: errors.length === 0, + errors: errors.length > 0 ? errors : undefined, + warnings: warnings.length > 0 ? warnings : undefined, + }; +} +``` + +#### `getAdditionalTemplateFiles(config)` +Generate extra files specific to your chain type. + +```typescript +getAdditionalTemplateFiles(config: CoinConfig): Array<{ src: string; dest: string }> { + return [ + { src: 'src/custom-builder.ts', dest: 'src/custom-builder.ts' }, + { src: 'src/custom-factory.ts', dest: 'src/custom-factory.ts' }, + ]; +} +``` + +#### `postGenerate(context, config)` +Run custom logic after generation. + +```typescript +async postGenerate(context: PluginContext, config: CoinConfig): Promise { + // Example: Create additional files + const customFile = path.join(context.destRoot, 'CUSTOM.md'); + await writeFile(customFile, 'Custom content for My Chain', 'utf-8'); + + // Example: Run chain-specific scripts + console.log(`Generated ${config.symbol} module for My Chain`); +} +``` + +## Helper Methods (From BaseChainPlugin) + +### `getVersionFromPackage(moduleName, contextRoot)` +Get version from a workspace package. + +```typescript +const version = await this.getVersionFromPackage('sdk-core', contextRoot); +// Returns: "^36.23.1" +``` + +### `getBaseDependencies(contextRoot)` +Get common dependencies for all chain types. + +```typescript +const baseDeps = await this.getBaseDependencies(contextRoot); +// Returns: +// { +// '@bitgo/sdk-core': '^36.23.1', +// '@bitgo/statics': '^58.16.1', +// 'bignumber.js': '^9.1.1' +// } +``` + +### `getBaseDevDependencies(contextRoot)` +Get common dev dependencies. + +```typescript +const baseDevDeps = await this.getBaseDevDependencies(contextRoot); +// Returns: +// { +// '@bitgo/sdk-api': '^1.71.9', +// '@bitgo/sdk-test': '^9.1.17' +// } +``` + +### `getTssDependencies(config, contextRoot)` +Get TSS-specific dependencies if enabled. + +```typescript +const tssDeps = await this.getTssDependencies(config, contextRoot); +// Returns (if TSS enabled): +// { +// '@bitgo/sdk-lib-mpc': '^10.8.1' +// } +// Returns empty object if TSS not enabled +``` + +## Complete Plugin Example + +```typescript +// plugins/polkadot-like.ts +import { BaseChainPlugin } from './base'; +import type { CoinConfig, DependencySet, ValidationResult, PluginContext } from '../core/types'; +import * as path from 'path'; +import { promisify } from 'util'; +import * as fs from 'fs'; + +const writeFile = promisify(fs.writeFile); + +export class PolkadotLikePlugin extends BaseChainPlugin { + readonly id = 'polkadot-like'; + readonly name = 'Polkadot-like'; + readonly description = 'Polkadot parachains and relay chains'; + readonly examples = ['DOT (Polkadot)', 'KSM (Kusama)', 'ASTR (Astar)']; + + getTemplateDir(): string { + // Reuse substrate-like templates + return 'generic-l1'; + } + + async getDependencies(config: CoinConfig, contextRoot: string): Promise { + return { + dependencies: { + ...(await this.getBaseDependencies(contextRoot)), + ...(await this.getTssDependencies(config, contextRoot)), + '@bitgo/abstract-substrate': await this.getVersionFromPackage('abstract-substrate', contextRoot), + '@polkadot/api': '14.1.1', + '@polkadot/types': '14.1.1', + }, + devDependencies: await this.getBaseDevDependencies(contextRoot), + }; + } + + validate(config: CoinConfig): ValidationResult { + const baseResult = super.validate(config); + const warnings = [...(baseResult.warnings || [])]; + + // Polkadot chains typically use ed25519 + if (config.keyCurve !== 'ed25519') { + warnings.push('Polkadot chains typically use ed25519 key curve'); + } + + return { + valid: baseResult.valid, + errors: baseResult.errors, + warnings: warnings.length > 0 ? warnings : undefined, + }; + } + + async postGenerate(context: PluginContext, config: CoinConfig): Promise { + // Create a Polkadot-specific README section + const readmePath = path.join(context.destRoot, 'POLKADOT.md'); + const content = `# Polkadot Integration Notes + +This module integrates with Polkadot using @polkadot/api. + +## Configuration + +Parachain ID: TBD +Relay chain: Polkadot/Kusama + +## Resources + +- [Polkadot Wiki](https://wiki.polkadot.network/) +- [@polkadot/api Documentation](https://polkadot.js.org/docs/api/) +`; + await writeFile(readmePath, content, 'utf-8'); + } +} +``` + +## Creating Custom Templates + +If your chain type needs different file structure, create custom templates: + +1. Create directory: `templates/my-chain/` +2. Copy files from `templates/generic-l1/` as a starting point +3. Customize files with your chain-specific logic +4. Use template variables: `<%= symbol %>`, `<%= constructor %>`, etc. + +### Available Template Variables + +- `<%= coin %>` - Coin name (e.g., "My New Chain") +- `<%= symbol %>` - Symbol (e.g., "mynew") +- `<%= testnetSymbol %>` - Testnet symbol (e.g., "tmynew") +- `<%= constructor %>` - PascalCase symbol (e.g., "Mynew") +- `<%= testnetConstructor %>` - PascalCase testnet symbol (e.g., "Tmynew") +- `<%= baseFactor %>` - Base factor (e.g., "1e18") +- `<%= keyCurve %>` - Key curve (e.g., "secp256k1") +- `<%= supportsTss %>` - TSS support (boolean) +- `<%= mpcAlgorithm %>` - MPC algorithm (e.g., "ecdsa") +- `<%= withTokenSupport %>` - Token support (boolean) + +## Testing Your Plugin + +1. **Generate a test coin**: +```bash +yarn sdk-coin:new-v2 +``` + +2. **Build the generated coin**: +```bash +cd modules/sdk-coin-{symbol} +npx tsc --build --incremental . +``` + +3. **Verify output**: +```bash +ls dist/src/ # Should have .js and .d.ts files +``` + +## Architecture + +``` +plugins/ +├── base.ts # Base plugin class with helpers +├── index.ts # Plugin registry +├── generic-l1.ts # Generic L1 plugin +├── evm-like.ts # EVM-like plugin +├── substrate-like.ts # Substrate-like plugin +├── cosmos.ts # Cosmos plugin +└── my-chain.ts # Your new plugin + +core/ +├── types.ts # TypeScript interfaces +├── generator.ts # Core generator (uses plugins) +└── prompts.ts # Interactive prompts (uses plugin registry) + +templates/ +├── generic-l1/ # Generic L1 templates +└── my-chain/ # Your custom templates (optional) +``` + +## Best Practices + +1. **Start with generic-l1 templates**: Reuse existing templates until you need customization +2. **Use helper methods**: Leverage `getBaseDependencies()`, `getTssDependencies()`, etc. +3. **Add validation**: Warn users about chain-specific requirements +4. **Provide examples**: List real coins using your chain type +5. **Test thoroughly**: Generate and build coins with your plugin +6. **Keep it simple**: Only add chain-specific dependencies and logic + +## Troubleshooting + +### Plugin not showing in prompts +- Check registration in `plugins/index.ts` +- Ensure plugin class is exported +- Verify `id` is unique + +### TypeScript errors +- Ensure plugin extends `BaseChainPlugin` +- Implement required `getDependencies()` method +- Return correct types (`DependencySet`, `ValidationResult`) + +### Template not found +- Check `getTemplateDir()` returns existing directory +- Verify templates exist in `templates/{templateDir}/` +- Use `generic-l1` if custom templates don't exist yet + +## Need Help? + +- Check existing plugins in `plugins/` directory +- Review `BaseChainPlugin` in `plugins/base.ts` +- See main [README.md](./README.md) for usage + +--- + +**Version**: 2.0.1 diff --git a/scripts/sdk-coin-generator-v2/README.md b/scripts/sdk-coin-generator-v2/README.md new file mode 100644 index 0000000000..e3628baf07 --- /dev/null +++ b/scripts/sdk-coin-generator-v2/README.md @@ -0,0 +1,333 @@ +# SDK Coin Generator V2 + +Generate a new BitGo SDK coin module for unique L1 blockchains. + +## Quick Start + +```bash +yarn sdk-coin:new-v2 +``` + +Follow the interactive prompts to generate your coin module. + +## What You'll Be Asked + +The generator will ask you for: + +1. **Coin name** - Full name (e.g., "Canton Coin", "Bittensor") +2. **Mainnet symbol** - Lowercase symbol (e.g., "canton", "tao") +3. **Testnet symbol** - Testnet symbol (default: "t{symbol}") +4. **Base factor** - Decimal conversion (e.g., "1e10", "1e18") +5. **Key curve** - Choose between: + - `ed25519` - Edwards-curve (Canton, TAO) + - `secp256k1` - ECDSA (ICP, Bitcoin-like) +6. **TSS support** - Whether the coin supports Threshold Signature Scheme +7. **MPC algorithm** - Auto-determined from key curve: + - `ed25519` → `eddsa` + - `secp256k1` → `ecdsa` +8. **Chain type** - Select from: + - `generic-l1` - Unique L1 blockchains (Canton, ICP) + - `evm-like` - Ethereum-compatible chains (Arbitrum, Polygon) + - `substrate-like` - Polkadot/Substrate chains (TAO, DOT) + - `cosmos` - Cosmos SDK chains (ATOM, Osmosis) +9. **Token support** - Whether to include token class + +## Example Session + +``` +🚀 BitGo SDK Coin Generator V2 + +📚 Examples of existing coins: + Generic L1: + • Canton (ed25519, TSS/eddsa) + • ICP (secp256k1, TSS/ecdsa) + EVM-like: + • Arbitrum + • Optimism + • Polygon + Substrate-like: + • TAO (ed25519) + • DOT + • Kusama + Cosmos: + • ATOM (Cosmos Hub) + • OSMO (Osmosis) + • TIA (Celestia) + +◆ What is the coin name? +│ My New Chain +│ +◆ What is the mainnet symbol? +│ mynew +│ +◆ What is the testnet symbol? +│ tmynew +│ +◆ What is the base factor? +│ 1e18 +│ +◆ Which key curve? +│ › ○ ed25519 (Edwards-curve) +│ ● secp256k1 (ECDSA) +│ +◆ Does it support TSS? +│ Yes +│ +◇ 🔐 MPC Algorithm +│ Auto-set to: ecdsa +│ +◆ What is the chain type? +│ › ○ Generic L1 (Unique L1 blockchains) +│ ● EVM-like (Ethereum Virtual Machine compatible) +│ ○ Substrate-like (Polkadot/Substrate based) +│ ○ Cosmos (Cosmos SDK chains) +│ +◆ Include token support? +│ No + +✓ Module files generated + +✅ Module created successfully +📁 Location: modules/sdk-coin-mynew + +Generated 22 files: + • package.json + • tsconfig.json + • README.md + • .eslintignore + • .gitignore + • .mocharc.yml + • .npmignore + • .prettierignore + • .prettierrc.yml + • src/index.ts + ... and 12 more + +📋 Next steps: + 1. Review generated files + 2. Add coin to statics configuration + 3. Register coin in BitGo module + 4. Update root tsconfig.packages.json + 5. cd modules/sdk-coin-mynew && yarn install && yarn build + 6. Run tests: yarn test + +✨ All done! Happy coding! +``` + +## What Gets Generated + +``` +modules/sdk-coin-{symbol}/ +├── package.json # Dependencies and scripts +├── tsconfig.json # TypeScript configuration +├── README.md # Module documentation +├── Configuration files +│ ├── .eslintignore +│ ├── .gitignore +│ ├── .mocharc.yml +│ ├── .npmignore +│ ├── .prettierignore +│ └── .prettierrc.yml +├── src/ +│ ├── index.ts # Main exports +│ ├── {symbol}.ts # Mainnet coin class +│ ├── t{symbol}.ts # Testnet coin class +│ ├── register.ts # Coin registration +│ └── lib/ +│ ├── index.ts +│ ├── keyPair.ts # Key pair management +│ ├── utils.ts # Utility functions +│ ├── constants.ts # Coin constants +│ └── iface.ts # TypeScript interfaces +└── test/ + ├── unit/ + │ ├── index.ts # Coin tests + │ ├── keyPair.ts # Key pair tests + │ └── utils.ts # Utility tests + └── integration/ + └── index.ts # Integration test placeholder +``` + +## Dependencies by Chain Type + +### Generic L1 +Basic dependencies for unique L1 blockchains: +- `@bitgo/sdk-core` +- `@bitgo/statics` +- `bignumber.js` +- `@bitgo/sdk-lib-mpc` (if TSS enabled) + +### EVM-like +EVM-compatible chains add: +- All generic L1 dependencies +- `@bitgo/abstract-eth` + +### Substrate-like +Substrate-based chains add: +- All generic L1 dependencies +- `@bitgo/abstract-substrate` +- `@polkadot/api` +- `@substrate/txwrapper-core` +- `@substrate/txwrapper-polkadot` + +### Cosmos +Cosmos SDK chains add: +- All generic L1 dependencies +- `@bitgo/abstract-cosmos` + +## After Generation + +### 1. Review Generated Files +Check all generated files and look for `TODO` comments marking areas that need implementation. + +### 2. Add to Statics +Add coin configuration to `modules/statics/src/coins.ts`: + +```typescript +export const coins = CoinMap.fromCoins([ + // ... existing coins + { + id: 'mynew', + name: 'My New Chain', + fullName: 'My New Chain', + network: { + type: 'mynew', + family: 'mynew', + }, + features: ['valueless', 'tss'], + baseFactor: '1e18', + decimalPlaces: 18, + isToken: false, + }, + { + id: 'tmynew', + name: 'Testnet My New Chain', + fullName: 'Testnet My New Chain', + network: { + type: 'tmynew', + family: 'mynew', + }, + features: ['valueless', 'tss'], + baseFactor: '1e18', + decimalPlaces: 18, + isToken: false, + }, +]); +``` + +### 3. Register in BitGo +Update `modules/bitgo/src/v2/coins/index.ts`: + +```typescript +import { Mynew } from '@bitgo/sdk-coin-mynew'; + +// In register() function: +GlobalCoinFactory.register('mynew', Mynew.createInstance); +GlobalCoinFactory.register('tmynew', Tmynew.createInstance); +``` + +### 4. Update Root Config +Add to `tsconfig.packages.json`: + +```json +{ + "references": [ + // ... existing references + { "path": "./modules/sdk-coin-mynew" } + ] +} +``` + +### 5. Install and Build +```bash +cd modules/sdk-coin-mynew +yarn install +yarn build +``` + +### 6. Run Tests +```bash +yarn test +``` + +### 7. Implement Core Logic +Replace placeholder implementations in: +- `src/lib/utils.ts` - Address validation, public key validation +- `src/lib/keyPair.ts` - Key pair generation and management +- `src/{symbol}.ts` - Transaction building, signing, parsing + +### 8. Write Tests +Complete the test suite in: +- `test/unit/index.ts` - Coin class tests +- `test/unit/keyPair.ts` - Key pair tests +- `test/unit/utils.ts` - Utility function tests + +## Validation + +The generator validates: +- Symbol format (lowercase alphanumeric) +- Module doesn't already exist +- Valid base factor expression +- MPC algorithm matches key curve +- All required inputs provided + +## Supported Chains + +### Generic L1 +Unique layer-1 blockchains that don't fit other categories. + +**Examples**: Canton, ICP + +### EVM-like +Ethereum Virtual Machine compatible chains. + +**Examples**: Arbitrum, Optimism, Polygon, Avalanche C-Chain + +### Substrate-like +Polkadot/Substrate-based chains. + +**Examples**: TAO (Bittensor), DOT (Polkadot), KSM (Kusama) + +### Cosmos +Cosmos SDK chains using Tendermint/CometBFT consensus. + +**Examples**: ATOM (Cosmos Hub), OSMO (Osmosis), TIA (Celestia) + +## Troubleshooting + +### Module Already Exists +If you see "Module sdk-coin-{symbol} already exists": +- Choose a different symbol, or +- Delete the existing module if it was a test + +### Invalid Symbol +Symbols must be: +- Lowercase +- Alphanumeric only +- Start with a letter + +Valid: `canton`, `icp`, `mynew`, `testcoin` +Invalid: `Canton`, `test-coin`, `123coin`, `my_coin` + +### Dependencies Not Found +If dependency versions can't be resolved: +- Ensure you're running from the BitGoJS root directory +- Verify the referenced modules exist in `modules/` + +## Tips + +- **Use existing coins as reference**: Look at Canton (`sdk-coin-canton`) for generic L1, or TAO (`sdk-coin-tao`) for Substrate-like chains +- **Start simple**: Implement basic functionality first, then add advanced features +- **Follow existing patterns**: The generated code follows patterns from existing modules +- **Test thoroughly**: Write comprehensive unit tests before integration tests + +## Need Help? + +- Check existing coin implementations in `modules/` +- Review BitGo SDK Core documentation +- See [PLUGIN_DEVELOPMENT.md](./PLUGIN_DEVELOPMENT.md) if you want to add a new chain type + +--- + +**Version**: 2.0.1 +**Generated modules version**: 1.0.0 diff --git a/scripts/sdk-coin-generator-v2/core/generator.ts b/scripts/sdk-coin-generator-v2/core/generator.ts new file mode 100644 index 0000000000..dd0351f24e --- /dev/null +++ b/scripts/sdk-coin-generator-v2/core/generator.ts @@ -0,0 +1,358 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { promisify } from 'util'; +import * as p from '@clack/prompts'; +import type { CoinConfig, TemplateData, PluginContext } from './types'; +import { PluginRegistry } from '../plugins'; + +const readFile = promisify(fs.readFile); +const writeFile = promisify(fs.writeFile); +const mkdir = promisify(fs.mkdir); + +/** + * Core generator class that orchestrates module generation using plugins + */ +export class Generator { + private registry: PluginRegistry; + private contextRoot: string; + + constructor(registry: PluginRegistry, contextRoot?: string) { + this.registry = registry; + this.contextRoot = contextRoot || path.join(__dirname, '..', '..', '..'); + } + + /** + * Generate a new SDK coin module + */ + async generate(config: CoinConfig): Promise { + // Get the plugin for this chain type + const plugin = this.registry.getRequired(config.chainType); + + // Validate configuration with core validation + this.validateConfig(config); + + // Validate with plugin-specific validation + const pluginValidation = plugin.validate(config); + if (!pluginValidation.valid) { + throw new Error(`Configuration validation failed: ${pluginValidation.errors?.join(', ')}`); + } + + // Show warnings if any + if (pluginValidation.warnings && pluginValidation.warnings.length > 0) { + pluginValidation.warnings.forEach((warning) => p.log.warn(warning)); + } + + // Get template directory from plugin + const templateDir = plugin.getTemplateDir(); + const templateRoot = path.join(__dirname, '..', 'templates', templateDir); + const destRoot = path.join(this.contextRoot, 'modules', `sdk-coin-${config.symbol}`); + + const context: PluginContext = { + contextRoot: this.contextRoot, + templateRoot, + destRoot, + }; + + // Prepare template data + const data = this.prepareTemplateData(config); + + const s = p.spinner(); + s.start('Generating module files...'); + + // Create directory structure + await this.createDirectoryStructure(destRoot); + + let fileCount = 0; + const files: string[] = []; + + // Generate package.json + await this.generatePackageJson(config, plugin, destRoot); + fileCount++; + files.push('package.json'); + + // Copy template files + const copiedFiles = await this.copyTemplateFiles(templateRoot, destRoot, data, config); + fileCount += copiedFiles.length; + files.push(...copiedFiles); + + // Generate token class if requested + if (config.withTokenSupport) { + const tokenFile = await this.generateTokenClass(templateRoot, destRoot, data, config); + if (tokenFile) { + fileCount++; + files.push(tokenFile); + } + } + + // Call plugin's post-generation hook if available + if (plugin.postGenerate) { + await plugin.postGenerate(context, config); + } + + s.stop('Module files generated'); + + // Display success message + this.displaySuccessMessage(config, fileCount, files); + } + + /** + * Validate core configuration + */ + private validateConfig(config: CoinConfig): void { + // Prevent path traversal attacks + if (config.symbol.includes('..') || config.symbol.includes('/') || config.symbol.includes('\\')) { + throw new Error('Symbol cannot contain path traversal characters'); + } + if (config.testnetSymbol.includes('..') || config.testnetSymbol.includes('/') || config.testnetSymbol.includes('\\')) { + throw new Error('Testnet symbol cannot contain path traversal characters'); + } + + // Validate symbol format (already validated in prompts, but double-check for security) + if (!/^[a-z][a-z0-9]*$/.test(config.symbol)) { + throw new Error('Symbol must be lowercase alphanumeric and start with a letter'); + } + if (!/^[a-z][a-z0-9]*$/.test(config.testnetSymbol)) { + throw new Error('Testnet symbol must be lowercase alphanumeric and start with a letter'); + } + + // Check if module already exists + const destRoot = path.join(this.contextRoot, 'modules', `sdk-coin-${config.symbol}`); + if (fs.existsSync(destRoot)) { + throw new Error(`Module sdk-coin-${config.symbol} already exists!`); + } + + // Validate chain type exists + if (!this.registry.has(config.chainType)) { + throw new Error( + `Unknown chain type: ${config.chainType}. Available types: ${this.registry.getPluginIds().join(', ')}` + ); + } + } + + /** + * Prepare template data from config + */ + private prepareTemplateData(config: CoinConfig): TemplateData { + return { + coin: config.coinName, + coinLowerCase: config.coinName.toLowerCase(), + symbol: config.symbol, + testnetSymbol: config.testnetSymbol, + constructor: this.toPascalCase(config.symbol), + testnetConstructor: this.toPascalCase(config.testnetSymbol), + baseFactor: config.baseFactor, + keyCurve: config.keyCurve, + supportsTss: config.supportsTss, + mpcAlgorithm: config.mpcAlgorithm || 'eddsa', + withTokenSupport: config.withTokenSupport, + }; + } + + /** + * Convert string to PascalCase + */ + private toPascalCase(str: string): string { + return str + .split(/[-_]/) + .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) + .join(''); + } + + /** + * Render a template with data + */ + private renderTemplate(template: string, data: TemplateData): string { + let rendered = template; + + Object.entries(data).forEach(([key, value]) => { + // Escape regex special characters to prevent RegExp injection + const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const regex = new RegExp(`<%=\\s*${escapedKey}\\s*%>`, 'g'); + rendered = rendered.replace(regex, String(value)); + }); + + return rendered; + } + + /** + * Create directory structure + */ + private async createDirectoryStructure(destRoot: string): Promise { + await mkdir(destRoot, { recursive: true }); + await mkdir(path.join(destRoot, 'src', 'lib'), { recursive: true }); + await mkdir(path.join(destRoot, 'test', 'unit'), { recursive: true }); + await mkdir(path.join(destRoot, 'test', 'integration'), { recursive: true }); + } + + /** + * Generate package.json + */ + private async generatePackageJson( + config: CoinConfig, + plugin: ReturnType, + destRoot: string + ): Promise { + const { dependencies, devDependencies } = await plugin.getDependencies(config, this.contextRoot); + + const packageJson = { + name: `@bitgo/sdk-coin-${config.symbol}`, + version: '1.0.0', + description: `BitGo SDK coin library for ${config.coinName}`, + main: './dist/src/index.js', + types: './dist/src/index.d.ts', + scripts: { + build: 'yarn tsc --build --incremental --verbose .', + fmt: 'prettier --write .', + 'check-fmt': "prettier --check '**/*.{ts,js,json}'", + clean: 'rm -r ./dist', + lint: 'eslint --quiet .', + prepare: 'npm run build', + test: 'npm run coverage', + coverage: 'nyc -- npm run unit-test', + 'unit-test': 'mocha', + }, + author: 'BitGo SDK Team ', + license: 'MIT', + engines: { + node: '>=20 <23', + }, + repository: { + type: 'git', + url: 'https://github.com/BitGo/BitGoJS.git', + directory: `modules/sdk-coin-${config.symbol}`, + }, + 'lint-staged': { + '*.{js,ts}': ['yarn prettier --write', 'yarn eslint --fix'], + }, + publishConfig: { + access: 'public', + }, + nyc: { + extension: ['.ts'], + }, + dependencies, + devDependencies, + files: ['dist'], + }; + + const packagePath = path.join(destRoot, 'package.json'); + await writeFile(packagePath, JSON.stringify(packageJson, null, 2) + '\n', 'utf-8'); + return packagePath; + } + + /** + * Copy template files + * + * Template files with EJS tags use .ejs extension (e.g., coin.ts.ejs) + * to prevent CodeQL, ESLint, and Prettier from analyzing them as TypeScript. + * The generated files will have the proper extension (e.g., .ts, .md). + */ + private async copyTemplateFiles( + templateRoot: string, + destRoot: string, + data: TemplateData, + config: CoinConfig + ): Promise { + const files: string[] = []; + + const templateFiles = [ + { src: '.tsconfig.json', dest: 'tsconfig.json' }, + { src: 'README.md.ejs', dest: 'README.md' }, + { src: '.eslintignore', dest: '.eslintignore' }, + { src: '.gitignore', dest: '.gitignore' }, + { src: '.mocharc.yml', dest: '.mocharc.yml' }, + { src: '.npmignore', dest: '.npmignore' }, + { src: '.prettierignore', dest: '.prettierignore' }, + { src: '.prettierrc.yml', dest: '.prettierrc.yml' }, + { src: 'src/index.ts.ejs', dest: 'src/index.ts' }, + { src: 'src/coin.ts.ejs', dest: `src/${config.symbol}.ts` }, + { src: 'src/testnet.ts.ejs', dest: `src/${config.testnetSymbol}.ts` }, + { src: 'src/register.ts.ejs', dest: 'src/register.ts' }, + { src: 'src/lib/index.ts', dest: 'src/lib/index.ts' }, + { src: 'src/lib/keyPair.ts.ejs', dest: 'src/lib/keyPair.ts' }, + { src: 'src/lib/utils.ts.ejs', dest: 'src/lib/utils.ts' }, + { src: 'src/lib/constants.ts.ejs', dest: 'src/lib/constants.ts' }, + { src: 'src/lib/iface.ts.ejs', dest: 'src/lib/iface.ts' }, + { src: 'test/unit/index.ts.ejs', dest: 'test/unit/index.ts' }, + { src: 'test/unit/keyPair.ts.ejs', dest: 'test/unit/keyPair.ts' }, + { src: 'test/unit/utils.ts.ejs', dest: 'test/unit/utils.ts' }, + { src: 'test/integration/index.ts.ejs', dest: 'test/integration/index.ts' }, + ]; + + for (const file of templateFiles) { + const templatePath = path.join(templateRoot, file.src); + const destPath = path.join(destRoot, file.dest); + + if (fs.existsSync(templatePath)) { + await this.copyTemplate(templatePath, destPath, data); + files.push(file.dest); + } + } + + return files; + } + + /** + * Copy a single template file + */ + private async copyTemplate(templatePath: string, destPath: string, data: TemplateData): Promise { + const template = await readFile(templatePath, 'utf-8'); + const rendered = this.renderTemplate(template, data); + await writeFile(destPath, rendered, 'utf-8'); + } + + /** + * Generate token class if requested + */ + private async generateTokenClass( + templateRoot: string, + destRoot: string, + data: TemplateData, + config: CoinConfig + ): Promise { + // Try .ejs extension first, then fallback to .ts for backward compatibility + let tokenTemplatePath = path.join(templateRoot, 'src/token.ts.ejs'); + if (!fs.existsSync(tokenTemplatePath)) { + tokenTemplatePath = path.join(templateRoot, 'src/token.ts'); + } + + const tokenDestPath = path.join(destRoot, 'src', `${config.symbol}Token.ts`); + + if (fs.existsSync(tokenTemplatePath)) { + await this.copyTemplate(tokenTemplatePath, tokenDestPath, data); + return `src/${config.symbol}Token.ts`; + } + + return null; + } + + /** + * Display success message + */ + private displaySuccessMessage(config: CoinConfig, fileCount: number, files: string[]): void { + p.note( + `📁 Location: modules/sdk-coin-${config.symbol}\n\n` + + `Generated ${fileCount} files:\n` + + files + .slice(0, 10) + .map((f) => ` • ${f}`) + .join('\n') + + (files.length > 10 ? `\n ... and ${files.length - 10} more` : ''), + '✅ Module created successfully' + ); + + const nextSteps = [ + 'Review generated files', + 'Add coin to statics configuration', + 'Register coin in BitGo module', + 'Update root tsconfig.packages.json', + `cd modules/sdk-coin-${config.symbol} && yarn install && yarn build`, + 'Run tests: yarn test', + ]; + + p.note( + nextSteps.map((step, i) => `${i + 1}. ${step}`).join('\n'), + '📋 Next steps' + ); + } +} diff --git a/scripts/sdk-coin-generator-v2/core/prompts.ts b/scripts/sdk-coin-generator-v2/core/prompts.ts new file mode 100644 index 0000000000..8f14d05d0e --- /dev/null +++ b/scripts/sdk-coin-generator-v2/core/prompts.ts @@ -0,0 +1,129 @@ +import * as p from '@clack/prompts'; +import type { CoinConfig } from './types'; +import { PluginRegistry } from '../plugins'; + +/** + * Prompt user for coin configuration + */ +export async function promptUser(registry: PluginRegistry): Promise { + console.clear(); + + p.intro('🚀 BitGo SDK Coin Generator V2'); + + // Display examples from all registered plugins + const examples = registry.getAllExamples(); + const exampleText = examples + .map((e) => `${e.chainType}:\n${e.examples.map((ex) => ` • ${ex}`).join('\n')}`) + .join('\n\n'); + + p.note(exampleText, '📚 Examples of existing coins'); + + const group = await p.group( + { + coinName: () => + p.text({ + message: 'What is the coin name?', + placeholder: 'Canton Coin', + validate: (value) => { + if (!value) return 'Coin name is required'; + }, + }), + symbol: () => + p.text({ + message: 'What is the mainnet symbol?', + placeholder: 'canton', + validate: (value) => { + if (!value) return 'Symbol is required'; + if (!/^[a-z][a-z0-9]*$/.test(value)) { + return 'Symbol must be lowercase alphanumeric and start with a letter'; + } + }, + }), + testnetSymbol: ({ results }) => + p.text({ + message: 'What is the testnet symbol?', + placeholder: `t${results.symbol}`, + initialValue: `t${results.symbol}`, + validate: (value) => { + if (!value) return 'Testnet symbol is required'; + if (!/^[a-z][a-z0-9]*$/.test(value)) { + return 'Testnet symbol must be lowercase alphanumeric and start with a letter'; + } + }, + }), + baseFactor: () => + p.text({ + message: 'What is the base factor?', + placeholder: '1e10', + validate: (value) => { + if (!value) return 'Base factor is required'; + + // Only allow safe numeric formats: plain numbers or scientific notation (e.g., 1e10, 1.5e18) + if (!/^[0-9]+(\.[0-9]+)?(e[0-9]+)?$/i.test(value)) { + return 'Base factor must be a number or scientific notation (e.g., 1e10, 1.5e18)'; + } + + try { + const factor = Number(value); + if (isNaN(factor) || factor <= 0 || !isFinite(factor)) { + return 'Base factor must be a positive number'; + } + } catch { + return 'Invalid base factor'; + } + }, + }), + keyCurve: () => + p.select({ + message: 'Which key curve?', + options: [ + { value: 'ed25519', label: 'ed25519', hint: 'Edwards-curve (Canton, TAO)' }, + { value: 'secp256k1', label: 'secp256k1', hint: 'ECDSA (ICP, Bitcoin-like)' }, + ], + initialValue: 'ed25519', + }), + supportsTss: () => + p.confirm({ + message: 'Does it support TSS?', + initialValue: true, + }), + mpcAlgorithm: ({ results }) => { + if (!results.supportsTss) return Promise.resolve(undefined); + + // Auto-determine based on key curve + const autoMpc = results.keyCurve === 'ed25519' ? 'eddsa' : 'ecdsa'; + p.note(`Auto-set to: ${autoMpc}`, '🔐 MPC Algorithm'); + return Promise.resolve(autoMpc as 'eddsa' | 'ecdsa'); + }, + chainType: () => + p.select({ + message: 'What is the chain type?', + options: registry.getChainTypeOptions(), + initialValue: 'generic-l1', + }), + withTokenSupport: () => + p.confirm({ + message: 'Include token support?', + initialValue: false, + }), + }, + { + onCancel: () => { + p.cancel('Operation cancelled'); + process.exit(0); + }, + } + ); + + return { + coinName: group.coinName as string, + symbol: (group.symbol as string).toLowerCase(), + testnetSymbol: (group.testnetSymbol as string).toLowerCase(), + baseFactor: group.baseFactor as string, + keyCurve: group.keyCurve as 'ed25519' | 'secp256k1', + supportsTss: group.supportsTss as boolean, + mpcAlgorithm: group.mpcAlgorithm as 'eddsa' | 'ecdsa' | undefined, + chainType: group.chainType as string, + withTokenSupport: group.withTokenSupport as boolean, + }; +} diff --git a/scripts/sdk-coin-generator-v2/core/types.ts b/scripts/sdk-coin-generator-v2/core/types.ts new file mode 100644 index 0000000000..ccbab4c5dd --- /dev/null +++ b/scripts/sdk-coin-generator-v2/core/types.ts @@ -0,0 +1,52 @@ +/** + * Core types for SDK Coin Generator V2 + */ + +export interface CoinConfig { + coinName: string; + symbol: string; + testnetSymbol: string; + baseFactor: string; + keyCurve: 'ed25519' | 'secp256k1'; + supportsTss: boolean; + mpcAlgorithm?: 'eddsa' | 'ecdsa'; + chainType: string; + withTokenSupport: boolean; +} + +export interface TemplateData { + coin: string; + coinLowerCase: string; + symbol: string; + testnetSymbol: string; + constructor: string; + testnetConstructor: string; + baseFactor: string; + keyCurve: string; + supportsTss: boolean; + mpcAlgorithm: string; + withTokenSupport: boolean; +} + +export interface DependencySet { + dependencies: Record; + devDependencies: Record; +} + +export interface ChainTypeOption { + value: string; + label: string; + hint: string; +} + +export interface PluginContext { + contextRoot: string; + templateRoot: string; + destRoot: string; +} + +export interface ValidationResult { + valid: boolean; + errors?: string[]; + warnings?: string[]; +} diff --git a/scripts/sdk-coin-generator-v2/index.ts b/scripts/sdk-coin-generator-v2/index.ts new file mode 100755 index 0000000000..e7fa136194 --- /dev/null +++ b/scripts/sdk-coin-generator-v2/index.ts @@ -0,0 +1,67 @@ +#!/usr/bin/env node + +import * as p from '@clack/prompts'; +import { defaultRegistry } from './plugins'; +import { Generator } from './core/generator'; +import { promptUser } from './core/prompts'; + +/** + * SDK Coin Generator V2 - Main Entry Point + * + * This generator uses a modular, plugin-based architecture: + * - Plugins: Each chain type (generic-l1, evm-like, substrate-like, cosmos) is a plugin + * - Registry: Manages all available plugins + * - Generator: Core logic that orchestrates module generation using plugins + * - Prompts: Interactive CLI for gathering user input + * + * To add a new chain type: + * 1. Create a new plugin class extending BaseChainPlugin + * 2. Implement the required methods (getDependencies, validate, etc.) + * 3. Register it in the PluginRegistry + * + * Example: + * ```typescript + * import { BaseChainPlugin } from './plugins/base'; + * + * class MyChainPlugin extends BaseChainPlugin { + * readonly id = 'my-chain'; + * readonly name = 'My Chain'; + * readonly description = 'My custom chain type'; + * readonly examples = ['Example1', 'Example2']; + * + * async getDependencies(config, contextRoot) { + * return { + * dependencies: { ... }, + * devDependencies: { ... } + * }; + * } + * } + * + * // Register the plugin + * defaultRegistry.register(new MyChainPlugin()); + * ``` + */ +async function main() { + try { + // Get user input using interactive prompts + const config = await promptUser(defaultRegistry); + + // Generate the module using the core generator + const generator = new Generator(defaultRegistry); + await generator.generate(config); + + p.outro('✨ All done! Happy coding!'); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + p.log.error(`Generation failed: ${errorMessage}`); + + // Log stack trace for debugging if available + if (error instanceof Error && error.stack) { + p.log.error(error.stack); + } + + process.exit(1); + } +} + +main(); diff --git a/scripts/sdk-coin-generator-v2/plugins/base.ts b/scripts/sdk-coin-generator-v2/plugins/base.ts new file mode 100644 index 0000000000..fef5e9d485 --- /dev/null +++ b/scripts/sdk-coin-generator-v2/plugins/base.ts @@ -0,0 +1,161 @@ +import * as path from 'path'; +import * as fs from 'fs'; +import { promisify } from 'util'; +import type { CoinConfig, DependencySet, PluginContext, ValidationResult, ChainTypeOption } from '../core/types'; + +const readFile = promisify(fs.readFile); + +/** + * Base plugin interface that all chain type plugins must implement + */ +export interface IChainPlugin { + /** + * Unique identifier for this chain type + */ + readonly id: string; + + /** + * Display name for this chain type + */ + readonly name: string; + + /** + * Description/hint for the chain type + */ + readonly description: string; + + /** + * Examples of coins using this chain type + */ + readonly examples: string[]; + + /** + * Get the template directory name for this chain type + */ + getTemplateDir(): string; + + /** + * Get dependencies specific to this chain type + */ + getDependencies(config: CoinConfig, contextRoot: string): Promise; + + /** + * Validate configuration specific to this chain type + */ + validate(config: CoinConfig): ValidationResult; + + /** + * Get additional template files specific to this chain type (optional) + */ + getAdditionalTemplateFiles?(config: CoinConfig): Array<{ src: string; dest: string }>; + + /** + * Post-generation hook for chain-specific actions (optional) + */ + postGenerate?(context: PluginContext, config: CoinConfig): Promise; +} + +/** + * Abstract base class providing common functionality for all chain plugins + */ +export abstract class BaseChainPlugin implements IChainPlugin { + abstract readonly id: string; + abstract readonly name: string; + abstract readonly description: string; + abstract readonly examples: string[]; + + /** + * Get template directory name (defaults to plugin id) + */ + getTemplateDir(): string { + return this.id; + } + + /** + * Get version from a package in the workspace + */ + protected async getVersionFromPackage(moduleName: string, contextRoot: string): Promise { + try { + const packagePath = path.join(contextRoot, 'modules', moduleName, 'package.json'); + const content = await readFile(packagePath, 'utf-8'); + const pkg = JSON.parse(content); + return `^${pkg.version}`; + } catch (error) { + return '^1.0.0'; + } + } + + /** + * Get base dependencies common to all chain types + */ + protected async getBaseDependencies(contextRoot: string): Promise> { + return { + '@bitgo/sdk-core': await this.getVersionFromPackage('sdk-core', contextRoot), + '@bitgo/statics': await this.getVersionFromPackage('statics', contextRoot), + 'bignumber.js': '^9.1.1', + }; + } + + /** + * Get base dev dependencies common to all chain types + */ + protected async getBaseDevDependencies(contextRoot: string): Promise> { + return { + '@bitgo/sdk-api': await this.getVersionFromPackage('sdk-api', contextRoot), + '@bitgo/sdk-test': await this.getVersionFromPackage('sdk-test', contextRoot), + }; + } + + /** + * Get TSS-specific dependencies + */ + protected async getTssDependencies(config: CoinConfig, contextRoot: string): Promise> { + if (!config.supportsTss || !config.mpcAlgorithm) { + return {}; + } + + return { + '@bitgo/sdk-lib-mpc': await this.getVersionFromPackage('sdk-lib-mpc', contextRoot), + }; + } + + /** + * Base validation - can be extended by subclasses + */ + validate(config: CoinConfig): ValidationResult { + const errors: string[] = []; + const warnings: string[] = []; + + // Validate MPC algorithm matches key curve when TSS is enabled + if (config.supportsTss && config.mpcAlgorithm) { + const expectedMpc = config.keyCurve === 'ed25519' ? 'eddsa' : 'ecdsa'; + if (config.mpcAlgorithm !== expectedMpc) { + warnings.push( + `Key curve ${config.keyCurve} typically uses ${expectedMpc}, but ${config.mpcAlgorithm} was specified` + ); + } + } + + return { + valid: errors.length === 0, + errors: errors.length > 0 ? errors : undefined, + warnings: warnings.length > 0 ? warnings : undefined, + }; + } + + /** + * Abstract method - must be implemented by subclasses + */ + abstract getDependencies(config: CoinConfig, contextRoot: string): Promise; + + /** + * Convert this plugin to a chain type option for prompts + */ + toChainTypeOption(): ChainTypeOption { + return { + value: this.id, + label: this.name, + hint: this.description, + }; + } +} diff --git a/scripts/sdk-coin-generator-v2/plugins/cosmos.ts b/scripts/sdk-coin-generator-v2/plugins/cosmos.ts new file mode 100644 index 0000000000..2d3562f225 --- /dev/null +++ b/scripts/sdk-coin-generator-v2/plugins/cosmos.ts @@ -0,0 +1,37 @@ +import { BaseChainPlugin } from './base'; +import type { CoinConfig, DependencySet } from '../core/types'; + +/** + * Plugin for Cosmos SDK chains + * + * Examples: ATOM (Cosmos Hub), OSMO (Osmosis), TIA (Celestia) + * + * Cosmos SDK chains use the Cosmos SDK framework and typically + * use Tendermint/CometBFT consensus. + */ +export class CosmosPlugin extends BaseChainPlugin { + readonly id = 'cosmos'; + readonly name = 'Cosmos'; + readonly description = 'Cosmos SDK chains'; + readonly examples = ['ATOM (Cosmos Hub)', 'OSMO (Osmosis)', 'TIA (Celestia)']; + + /** + * Cosmos chains use the generic-l1 template for now + * TODO: Create Cosmos-specific templates with abstract-cosmos patterns + */ + getTemplateDir(): string { + return 'generic-l1'; + } + + async getDependencies(config: CoinConfig, contextRoot: string): Promise { + const dependencies = { + ...(await this.getBaseDependencies(contextRoot)), + ...(await this.getTssDependencies(config, contextRoot)), + '@bitgo/abstract-cosmos': await this.getVersionFromPackage('abstract-cosmos', contextRoot), + }; + + const devDependencies = await this.getBaseDevDependencies(contextRoot); + + return { dependencies, devDependencies }; + } +} diff --git a/scripts/sdk-coin-generator-v2/plugins/evm-like.ts b/scripts/sdk-coin-generator-v2/plugins/evm-like.ts new file mode 100644 index 0000000000..12447bc000 --- /dev/null +++ b/scripts/sdk-coin-generator-v2/plugins/evm-like.ts @@ -0,0 +1,37 @@ +import { BaseChainPlugin } from './base'; +import type { CoinConfig, DependencySet } from '../core/types'; + +/** + * Plugin for EVM-like (Ethereum Virtual Machine compatible) chains + * + * Examples: Arbitrum, Optimism, Polygon, Avalanche C-Chain + * + * EVM-like chains are compatible with Ethereum's Virtual Machine and + * share similar transaction structures and tooling. + */ +export class EvmLikePlugin extends BaseChainPlugin { + readonly id = 'evm-like'; + readonly name = 'EVM-like'; + readonly description = 'Ethereum Virtual Machine compatible'; + readonly examples = ['Arbitrum', 'Optimism', 'Polygon']; + + /** + * EVM-like chains use the generic-l1 template for now + * TODO: Create EVM-specific templates with abstract-eth patterns + */ + getTemplateDir(): string { + return 'generic-l1'; + } + + async getDependencies(config: CoinConfig, contextRoot: string): Promise { + const dependencies = { + ...(await this.getBaseDependencies(contextRoot)), + ...(await this.getTssDependencies(config, contextRoot)), + '@bitgo/abstract-eth': await this.getVersionFromPackage('abstract-eth', contextRoot), + }; + + const devDependencies = await this.getBaseDevDependencies(contextRoot); + + return { dependencies, devDependencies }; + } +} diff --git a/scripts/sdk-coin-generator-v2/plugins/generic-l1.ts b/scripts/sdk-coin-generator-v2/plugins/generic-l1.ts new file mode 100644 index 0000000000..90ae4d331e --- /dev/null +++ b/scripts/sdk-coin-generator-v2/plugins/generic-l1.ts @@ -0,0 +1,28 @@ +import { BaseChainPlugin } from './base'; +import type { CoinConfig, DependencySet } from '../core/types'; + +/** + * Plugin for Generic L1 blockchains + * + * Examples: Canton, ICP + * + * Generic L1 chains are unique layer-1 blockchains that don't fit into + * other categories (EVM-like, Substrate-like, Cosmos). + */ +export class GenericL1Plugin extends BaseChainPlugin { + readonly id = 'generic-l1'; + readonly name = 'Generic L1'; + readonly description = 'Unique L1 blockchains'; + readonly examples = ['Canton (ed25519, TSS/eddsa)', 'ICP (secp256k1, TSS/ecdsa)']; + + async getDependencies(config: CoinConfig, contextRoot: string): Promise { + const dependencies = { + ...(await this.getBaseDependencies(contextRoot)), + ...(await this.getTssDependencies(config, contextRoot)), + }; + + const devDependencies = await this.getBaseDevDependencies(contextRoot); + + return { dependencies, devDependencies }; + } +} diff --git a/scripts/sdk-coin-generator-v2/plugins/index.ts b/scripts/sdk-coin-generator-v2/plugins/index.ts new file mode 100644 index 0000000000..c99623bcdd --- /dev/null +++ b/scripts/sdk-coin-generator-v2/plugins/index.ts @@ -0,0 +1,134 @@ +import type { IChainPlugin } from './base'; +import { GenericL1Plugin } from './generic-l1'; +import { EvmLikePlugin } from './evm-like'; +import { SubstrateLikePlugin } from './substrate-like'; +import { CosmosPlugin } from './cosmos'; +import type { ChainTypeOption } from '../core/types'; + +/** + * Plugin Registry + * + * Manages all available chain type plugins and provides + * plugin lookup and registration functionality. + */ +export class PluginRegistry { + private plugins: Map = new Map(); + + constructor() { + // Register default plugins + this.registerDefaultPlugins(); + } + + /** + * Register default chain type plugins + */ + private registerDefaultPlugins(): void { + this.register(new GenericL1Plugin()); + this.register(new EvmLikePlugin()); + this.register(new SubstrateLikePlugin()); + this.register(new CosmosPlugin()); + } + + /** + * Register a new plugin + * This allows users to add custom chain type plugins + */ + register(plugin: IChainPlugin): void { + if (this.plugins.has(plugin.id)) { + throw new Error(`Plugin with id '${plugin.id}' is already registered`); + } + this.plugins.set(plugin.id, plugin); + } + + /** + * Unregister a plugin by id + */ + unregister(pluginId: string): boolean { + return this.plugins.delete(pluginId); + } + + /** + * Get a plugin by id + */ + get(pluginId: string): IChainPlugin | undefined { + return this.plugins.get(pluginId); + } + + /** + * Get a plugin by id, throw error if not found + */ + getRequired(pluginId: string): IChainPlugin { + const plugin = this.get(pluginId); + if (!plugin) { + throw new Error(`Plugin '${pluginId}' not found. Available plugins: ${this.getPluginIds().join(', ')}`); + } + return plugin; + } + + /** + * Check if a plugin exists + */ + has(pluginId: string): boolean { + return this.plugins.has(pluginId); + } + + /** + * Get all registered plugin IDs + */ + getPluginIds(): string[] { + return Array.from(this.plugins.keys()); + } + + /** + * Get all registered plugins + */ + getAll(): IChainPlugin[] { + return Array.from(this.plugins.values()); + } + + /** + * Get chain type options for prompts + */ + getChainTypeOptions(): ChainTypeOption[] { + return this.getAll().map((plugin) => ({ + value: plugin.id, + label: plugin.name, + hint: plugin.description, + })); + } + + /** + * Get examples from all plugins for display + */ + getAllExamples(): Array<{ chainType: string; examples: string[] }> { + return this.getAll().map((plugin) => ({ + chainType: plugin.name, + examples: plugin.examples, + })); + } + + /** + * Clear all plugins (useful for testing) + */ + clear(): void { + this.plugins.clear(); + } + + /** + * Get the number of registered plugins + */ + get size(): number { + return this.plugins.size; + } +} + +/** + * Default plugin registry instance + */ +export const defaultRegistry = new PluginRegistry(); + +/** + * Re-export plugins for direct access if needed + */ +export { GenericL1Plugin, EvmLikePlugin, SubstrateLikePlugin, CosmosPlugin }; +export type { IChainPlugin } from './base'; diff --git a/scripts/sdk-coin-generator-v2/plugins/substrate-like.ts b/scripts/sdk-coin-generator-v2/plugins/substrate-like.ts new file mode 100644 index 0000000000..3e4f061c32 --- /dev/null +++ b/scripts/sdk-coin-generator-v2/plugins/substrate-like.ts @@ -0,0 +1,40 @@ +import { BaseChainPlugin } from './base'; +import type { CoinConfig, DependencySet } from '../core/types'; + +/** + * Plugin for Substrate-like chains (Polkadot/Substrate based) + * + * Examples: TAO (Bittensor), DOT (Polkadot), KSM (Kusama) + * + * Substrate-like chains use the Substrate framework and typically + * use Polkadot's ecosystem tooling. + */ +export class SubstrateLikePlugin extends BaseChainPlugin { + readonly id = 'substrate-like'; + readonly name = 'Substrate-like'; + readonly description = 'Polkadot/Substrate based'; + readonly examples = ['TAO (ed25519)', 'DOT', 'Kusama']; + + /** + * Substrate-like chains use the generic-l1 template for now + * TODO: Create Substrate-specific templates with abstract-substrate patterns + */ + getTemplateDir(): string { + return 'generic-l1'; + } + + async getDependencies(config: CoinConfig, contextRoot: string): Promise { + const dependencies = { + ...(await this.getBaseDependencies(contextRoot)), + ...(await this.getTssDependencies(config, contextRoot)), + '@bitgo/abstract-substrate': await this.getVersionFromPackage('abstract-substrate', contextRoot), + '@polkadot/api': '14.1.1', + '@substrate/txwrapper-core': '7.5.2', + '@substrate/txwrapper-polkadot': '7.5.2', + }; + + const devDependencies = await this.getBaseDevDependencies(contextRoot); + + return { dependencies, devDependencies }; + } +} diff --git a/scripts/sdk-coin-generator-v2/templates/generic-l1/.eslintignore b/scripts/sdk-coin-generator-v2/templates/generic-l1/.eslintignore new file mode 100644 index 0000000000..de4d1f007d --- /dev/null +++ b/scripts/sdk-coin-generator-v2/templates/generic-l1/.eslintignore @@ -0,0 +1,2 @@ +dist +node_modules diff --git a/scripts/sdk-coin-generator-v2/templates/generic-l1/.gitignore b/scripts/sdk-coin-generator-v2/templates/generic-l1/.gitignore new file mode 100644 index 0000000000..d62464f3e6 --- /dev/null +++ b/scripts/sdk-coin-generator-v2/templates/generic-l1/.gitignore @@ -0,0 +1,3 @@ +/dist +/node_modules +/.nyc_output diff --git a/scripts/sdk-coin-generator-v2/templates/generic-l1/.mocharc.yml b/scripts/sdk-coin-generator-v2/templates/generic-l1/.mocharc.yml new file mode 100644 index 0000000000..6dc40bf923 --- /dev/null +++ b/scripts/sdk-coin-generator-v2/templates/generic-l1/.mocharc.yml @@ -0,0 +1,6 @@ +require: + - ts-node/register + - source-map-support/register +spec: 'test/unit/**/*.ts' +timeout: 10000 +reporter: min diff --git a/scripts/sdk-coin-generator-v2/templates/generic-l1/.npmignore b/scripts/sdk-coin-generator-v2/templates/generic-l1/.npmignore new file mode 100644 index 0000000000..85677ec29f --- /dev/null +++ b/scripts/sdk-coin-generator-v2/templates/generic-l1/.npmignore @@ -0,0 +1,10 @@ +**/*.ts +!**/*.d.ts +src +test +tsconfig.json +tslint.json +.gitignore +.eslintignore +.mocharc.yml +.prettierignore diff --git a/scripts/sdk-coin-generator-v2/templates/generic-l1/.prettierignore b/scripts/sdk-coin-generator-v2/templates/generic-l1/.prettierignore new file mode 100644 index 0000000000..de4d1f007d --- /dev/null +++ b/scripts/sdk-coin-generator-v2/templates/generic-l1/.prettierignore @@ -0,0 +1,2 @@ +dist +node_modules diff --git a/scripts/sdk-coin-generator-v2/templates/generic-l1/.prettierrc.yml b/scripts/sdk-coin-generator-v2/templates/generic-l1/.prettierrc.yml new file mode 100644 index 0000000000..5198f5e019 --- /dev/null +++ b/scripts/sdk-coin-generator-v2/templates/generic-l1/.prettierrc.yml @@ -0,0 +1,3 @@ +printWidth: 120 +singleQuote: true +trailingComma: es5 diff --git a/scripts/sdk-coin-generator-v2/templates/generic-l1/.tsconfig.json b/scripts/sdk-coin-generator-v2/templates/generic-l1/.tsconfig.json new file mode 100644 index 0000000000..b967e45713 --- /dev/null +++ b/scripts/sdk-coin-generator-v2/templates/generic-l1/.tsconfig.json @@ -0,0 +1,26 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./", + "strictPropertyInitialization": false, + "esModuleInterop": true, + "typeRoots": ["../../types", "./node_modules/@types", "../../node_modules/@types"] + }, + "include": ["src/**/*", "test/**/*", "resources/**/*"], + "exclude": ["node_modules"], + "references": [ + { + "path": "../sdk-api" + }, + { + "path": "../sdk-core" + }, + { + "path": "../statics" + }, + { + "path": "../sdk-test" + } + ] +} diff --git a/scripts/sdk-coin-generator-v2/templates/generic-l1/README.md.ejs b/scripts/sdk-coin-generator-v2/templates/generic-l1/README.md.ejs new file mode 100644 index 0000000000..64f4739fa5 --- /dev/null +++ b/scripts/sdk-coin-generator-v2/templates/generic-l1/README.md.ejs @@ -0,0 +1,30 @@ +# BitGo sdk-coin-<%= symbol %> + +SDK coins provide a modular approach to a monolithic architecture. This and all BitGoJS SDK coins allow developers to use only the coins needed for a given project. + +## Installation + +All coins are loaded traditionally through the `bitgo` package. If you are using coins individually, you will be accessing the coin via the `@bitgo/sdk-api` package. + +In your project install both `@bitgo/sdk-api` and `@bitgo/sdk-coin-<%= symbol %>`. + +```shell +npm i @bitgo/sdk-api @bitgo/sdk-coin-<%= symbol %> +``` + +Next, you will be able to initialize an instance of "bitgo" through `@bitgo/sdk-api` instead of `bitgo`. + +```javascript +import { BitGoAPI } from '@bitgo/sdk-api'; +import { <%= constructor %> } from '@bitgo/sdk-coin-<%= symbol %>'; + +const sdk = new BitGoAPI(); + +sdk.register('<%= symbol %>', <%= constructor %>.createInstance); +``` + +## Development + +Most of the coin implementations are derived from `@bitgo/sdk-core`, `@bitgo/statics`, and coin specific packages. These implementations are used to interact with the BitGo API and BitGo platform services. + +You will notice that the basic version of common class extensions have been provided to you and must be resolved before the package build will succeed. Upon initiation of a given SDK coin, you will need to verify that your coin has been included in the root `tsconfig.packages.json` and that the linting, formatting, and testing succeeds when run both within the coin and from the root of BitGoJS. diff --git a/scripts/sdk-coin-generator-v2/templates/generic-l1/src/coin.ts.ejs b/scripts/sdk-coin-generator-v2/templates/generic-l1/src/coin.ts.ejs new file mode 100644 index 0000000000..51c9940b4d --- /dev/null +++ b/scripts/sdk-coin-generator-v2/templates/generic-l1/src/coin.ts.ejs @@ -0,0 +1,157 @@ +import { + AuditDecryptedKeyParams, + BaseCoin, + BitGoBase, + KeyPair, + ParsedTransaction, + ParseTransactionOptions, + SignedTransaction, + SignTransactionOptions, + VerifyAddressOptions, + VerifyTransactionOptions, + TransactionExplanation, +} from '@bitgo/sdk-core'; +import { BaseCoin as StaticsBaseCoin, coins } from '@bitgo/statics'; +import { KeyPair as <%= constructor %>KeyPair } from './lib/keyPair'; +import utils from './lib/utils'; + +export class <%= constructor %> extends BaseCoin { + protected readonly _staticsCoin: Readonly; + + protected constructor(bitgo: BitGoBase, staticsCoin?: Readonly) { + super(bitgo); + + if (!staticsCoin) { + throw new Error('missing required constructor parameter staticsCoin'); + } + + this._staticsCoin = staticsCoin; + } + + static createInstance(bitgo: BitGoBase, staticsCoin?: Readonly): BaseCoin { + return new <%= constructor %>(bitgo, staticsCoin); + } + + /** + * Factor between the coin's base unit and its smallest subdivision + */ + public getBaseFactor(): number { + return <%= baseFactor %>; + } + + public getChain(): string { + return '<%= symbol %>'; + } + + public getFamily(): string { + return '<%= symbol %>'; + } + + public getFullName(): string { + return '<%= coin %>'; + } + + /** + * Flag for sending value of 0 + * @returns {boolean} True if okay to send 0 value, false otherwise + */ + valuelessTransferAllowed(): boolean { + return false; + } + + /** + * Checks if this is a valid base58 or hex address + * @param address + */ + isValidAddress(address: string): boolean { + return utils.isValidAddress(address); + } + + /** + * Generate ed25519 key pair + * + * @param seed + * @returns {Object} object with generated pub, prv + */ + generateKeyPair(seed?: Buffer): KeyPair { + const keyPair = seed ? new <%= constructor %>KeyPair({ seed }) : new <%= constructor %>KeyPair(); + const keys = keyPair.getKeys(); + + if (!keys.prv) { + throw new Error('Missing prv in key generation.'); + } + + return { + pub: keys.pub, + prv: keys.prv, + }; + } + + /** + * Return boolean indicating whether input is valid public key for the coin. + * + * @param {String} pub the pub to be checked + * @returns {Boolean} is it valid? + */ + isValidPub(pub: string): boolean { + return utils.isValidPublicKey(pub); + } + + /** + * Verify that a transaction prebuild complies with the original intention + * @param params + * @param params.txPrebuild + * @param params.txParams + * @returns {boolean} + */ + async verifyTransaction(params: VerifyTransactionOptions): Promise { + // TODO: Implement transaction verification + throw new Error('Method not implemented.'); + } + + /** + * Check if address is a wallet address + * @param params + */ + async isWalletAddress(params: VerifyAddressOptions): Promise { + // TODO: Implement address verification + throw new Error('Method not implemented.'); + } + + /** + * Audit a decrypted private key for security purposes + * @param params + */ + async auditDecryptedKey(params: AuditDecryptedKeyParams): Promise { + // TODO: Implement key auditing logic if needed + // This method is typically used for security compliance + return Promise.resolve(); + } + + /** + * Parse a transaction from the raw transaction hex + * @param params + */ + async parseTransaction(params: ParseTransactionOptions): Promise { + // TODO: Implement transaction parsing + throw new Error('Method not implemented.'); + } + + /** + * Explain a transaction + * @param params + */ + async explainTransaction(params: Record): Promise { + // TODO: Implement transaction explanation + throw new Error('Method not implemented.'); + } + + /** + * Sign a transaction + * @param params + */ + async signTransaction(params: SignTransactionOptions): Promise { + // TODO: Implement transaction signing + throw new Error('Method not implemented.'); + } +} diff --git a/scripts/sdk-coin-generator-v2/templates/generic-l1/src/index.ts.ejs b/scripts/sdk-coin-generator-v2/templates/generic-l1/src/index.ts.ejs new file mode 100644 index 0000000000..1168cc248d --- /dev/null +++ b/scripts/sdk-coin-generator-v2/templates/generic-l1/src/index.ts.ejs @@ -0,0 +1,4 @@ +export * from './lib'; +export * from './<%= symbol %>'; +export * from './<%= testnetSymbol %>'; +export * from './register'; diff --git a/scripts/sdk-coin-generator-v2/templates/generic-l1/src/lib/constants.ts.ejs b/scripts/sdk-coin-generator-v2/templates/generic-l1/src/lib/constants.ts.ejs new file mode 100644 index 0000000000..5c984b4412 --- /dev/null +++ b/scripts/sdk-coin-generator-v2/templates/generic-l1/src/lib/constants.ts.ejs @@ -0,0 +1,9 @@ +/** + * Constants for <%= coin %> + */ + +export const MAINNET_COIN = '<%= symbol %>'; +export const TESTNET_COIN = '<%= testnetSymbol %>'; + +export const VALID_ADDRESS_REGEX = /^[A-Za-z0-9]+$/; // Update with actual address format +export const VALID_PUBLIC_KEY_REGEX = /^[A-Fa-f0-9]{64}$/; // Update with actual public key format diff --git a/scripts/sdk-coin-generator-v2/templates/generic-l1/src/lib/iface.ts.ejs b/scripts/sdk-coin-generator-v2/templates/generic-l1/src/lib/iface.ts.ejs new file mode 100644 index 0000000000..1825303418 --- /dev/null +++ b/scripts/sdk-coin-generator-v2/templates/generic-l1/src/lib/iface.ts.ejs @@ -0,0 +1,16 @@ +/** + * Interfaces for <%= coin %> + */ + +export interface TransactionData { + // TODO: Define transaction data structure +} + +export interface TransactionOutput { + address: string; + amount: string; +} + +export interface TransactionInput { + // TODO: Define transaction input structure +} diff --git a/scripts/sdk-coin-generator-v2/templates/generic-l1/src/lib/index.ts b/scripts/sdk-coin-generator-v2/templates/generic-l1/src/lib/index.ts new file mode 100644 index 0000000000..f97b3b961e --- /dev/null +++ b/scripts/sdk-coin-generator-v2/templates/generic-l1/src/lib/index.ts @@ -0,0 +1,4 @@ +export * from './keyPair'; +export * from './utils'; +export * from './constants'; +export * from './iface'; diff --git a/scripts/sdk-coin-generator-v2/templates/generic-l1/src/lib/keyPair.ts.ejs b/scripts/sdk-coin-generator-v2/templates/generic-l1/src/lib/keyPair.ts.ejs new file mode 100644 index 0000000000..cbc2502c19 --- /dev/null +++ b/scripts/sdk-coin-generator-v2/templates/generic-l1/src/lib/keyPair.ts.ejs @@ -0,0 +1,67 @@ +import { DefaultKeys, isPrivateKey, isPublicKey, isSeed, KeyPairOptions } from '@bitgo/sdk-core'; +import * as crypto from 'crypto'; + +/** + * <%= coin %> keys and address management + */ +export class KeyPair { + private keyPair: DefaultKeys; + + /** + * Public constructor. By default, creates a key pair with a random master seed. + * + * @param { KeyPairOptions } source Either a master seed, a private key, or a public key + */ + constructor(source?: KeyPairOptions) { + let seed: Buffer; + + if (!source) { + seed = crypto.randomBytes(32); + } else if (isSeed(source)) { + seed = source.seed; + } else if (isPrivateKey(source)) { + // TODO: Implement private key to keypair conversion + throw new Error('Private key import not yet implemented'); + } else if (isPublicKey(source)) { + // TODO: Implement public key import + throw new Error('Public key import not yet implemented'); + } else { + throw new Error('Invalid key pair options'); + } + + // TODO: Generate actual keypair from seed based on the coin's key derivation + this.keyPair = this.generateKeyPairFromSeed(seed); + } + + /** + * Generate a keypair from a seed + * @param seed + * @private + */ + private generateKeyPairFromSeed(seed: Buffer): DefaultKeys { + // TODO: Implement actual key generation for <%= coin %> + // This is a placeholder implementation + const prv = seed.toString('hex'); + const pub = crypto.createHash('sha256').update(seed).digest('hex'); + + return { + prv, + pub, + }; + } + + /** + * Get the public key + */ + getKeys(): DefaultKeys { + return this.keyPair; + } + + /** + * Get the address + */ + getAddress(): string { + // TODO: Implement address derivation from public key + return this.keyPair.pub; + } +} diff --git a/scripts/sdk-coin-generator-v2/templates/generic-l1/src/lib/utils.ts.ejs b/scripts/sdk-coin-generator-v2/templates/generic-l1/src/lib/utils.ts.ejs new file mode 100644 index 0000000000..7a653610fc --- /dev/null +++ b/scripts/sdk-coin-generator-v2/templates/generic-l1/src/lib/utils.ts.ejs @@ -0,0 +1,40 @@ +import { VALID_ADDRESS_REGEX, VALID_PUBLIC_KEY_REGEX } from './constants'; + +/** + * Utility functions for <%= coin %> + */ + +/** + * Check if the address is valid + * @param address + */ +export function isValidAddress(address: string): boolean { + // TODO: Implement proper address validation for <%= coin %> + return VALID_ADDRESS_REGEX.test(address); +} + +/** + * Check if the public key is valid + * @param publicKey + */ +export function isValidPublicKey(publicKey: string): boolean { + // TODO: Implement proper public key validation for <%= coin %> + return VALID_PUBLIC_KEY_REGEX.test(publicKey); +} + +/** + * Check if the private key is valid + * @param privateKey + */ +export function isValidPrivateKey(privateKey: string): boolean { + // TODO: Implement proper private key validation for <%= coin %> + return privateKey.length === 64; +} + +const utils = { + isValidAddress, + isValidPublicKey, + isValidPrivateKey, +}; + +export default utils; diff --git a/scripts/sdk-coin-generator-v2/templates/generic-l1/src/register.ts.ejs b/scripts/sdk-coin-generator-v2/templates/generic-l1/src/register.ts.ejs new file mode 100644 index 0000000000..49973373bb --- /dev/null +++ b/scripts/sdk-coin-generator-v2/templates/generic-l1/src/register.ts.ejs @@ -0,0 +1,8 @@ +import { BitGoBase } from '@bitgo/sdk-core'; +import { <%= constructor %> } from './<%= symbol %>'; +import { <%= testnetConstructor %> } from './<%= testnetSymbol %>'; + +export const register = (sdk: BitGoBase): void => { + sdk.register('<%= symbol %>', <%= constructor %>.createInstance); + sdk.register('<%= testnetSymbol %>', <%= testnetConstructor %>.createInstance); +}; diff --git a/scripts/sdk-coin-generator-v2/templates/generic-l1/src/testnet.ts.ejs b/scripts/sdk-coin-generator-v2/templates/generic-l1/src/testnet.ts.ejs new file mode 100644 index 0000000000..cbcbdbbb4e --- /dev/null +++ b/scripts/sdk-coin-generator-v2/templates/generic-l1/src/testnet.ts.ejs @@ -0,0 +1,21 @@ +import { BaseCoin, BitGoBase } from '@bitgo/sdk-core'; +import { BaseCoin as StaticsBaseCoin } from '@bitgo/statics'; +import { <%= constructor %> } from './<%= symbol %>'; + +export class <%= testnetConstructor %> extends <%= constructor %> { + protected constructor(bitgo: BitGoBase, staticsCoin?: Readonly) { + super(bitgo, staticsCoin); + } + + static createInstance(bitgo: BitGoBase, staticsCoin?: Readonly): BaseCoin { + return new <%= testnetConstructor %>(bitgo, staticsCoin); + } + + getChain() { + return '<%= testnetSymbol %>'; + } + + getFullName() { + return 'Testnet <%= coin %>'; + } +} diff --git a/scripts/sdk-coin-generator-v2/templates/generic-l1/test/integration/index.ts.ejs b/scripts/sdk-coin-generator-v2/templates/generic-l1/test/integration/index.ts.ejs new file mode 100644 index 0000000000..8e2d8af2db --- /dev/null +++ b/scripts/sdk-coin-generator-v2/templates/generic-l1/test/integration/index.ts.ejs @@ -0,0 +1,11 @@ +/** + * Integration tests for <%= coin %> + * + * These tests require a running <%= coin %> node or testnet connection + * and are typically run separately from unit tests. + * + * TODO: Add integration tests + */ + +// Placeholder to ensure test file loads correctly +export {}; diff --git a/scripts/sdk-coin-generator-v2/templates/generic-l1/test/unit/index.ts.ejs b/scripts/sdk-coin-generator-v2/templates/generic-l1/test/unit/index.ts.ejs new file mode 100644 index 0000000000..1917b35acb --- /dev/null +++ b/scripts/sdk-coin-generator-v2/templates/generic-l1/test/unit/index.ts.ejs @@ -0,0 +1,46 @@ +import { <%= constructor %> } from '../../src/<%= symbol %>'; +import { <%= testnetConstructor %> } from '../../src/<%= testnetSymbol %>'; +import { BitGoAPI } from '@bitgo/sdk-api'; +import { TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test'; + +describe('<%= coin %> Coin', function () { + let bitgo: TestBitGoAPI; + let basecoin; + + before(function () { + bitgo = TestBitGo.decorate(BitGoAPI, { env: 'mock' }); + bitgo.safeRegister('<%= symbol %>', <%= constructor %>.createInstance); + bitgo.initializeTestVars(); + basecoin = bitgo.coin('<%= symbol %>'); + }); + + it('should instantiate the coin', function () { + const basecoin = bitgo.coin('<%= symbol %>'); + basecoin.should.be.an.instanceof(<%= constructor %>); + }); + + it('should return the correct coin name', function () { + basecoin.getChain().should.equal('<%= symbol %>'); + basecoin.getFullName().should.equal('<%= coin %>'); + basecoin.getBaseFactor().should.equal(<%= baseFactor %>); + }); + + describe('Testnet', function () { + let testnetBasecoin; + + before(function () { + bitgo.safeRegister('<%= testnetSymbol %>', <%= testnetConstructor %>.createInstance); + testnetBasecoin = bitgo.coin('<%= testnetSymbol %>'); + }); + + it('should instantiate the testnet coin', function () { + testnetBasecoin.should.be.an.instanceof(<%= testnetConstructor %>); + }); + + it('should return the correct testnet coin name', function () { + testnetBasecoin.getChain().should.equal('<%= testnetSymbol %>'); + testnetBasecoin.getFullName().should.equal('Testnet <%= coin %>'); + testnetBasecoin.getBaseFactor().should.equal(<%= baseFactor %>); + }); + }); +}); diff --git a/scripts/sdk-coin-generator-v2/templates/generic-l1/test/unit/keyPair.ts.ejs b/scripts/sdk-coin-generator-v2/templates/generic-l1/test/unit/keyPair.ts.ejs new file mode 100644 index 0000000000..fc21234855 --- /dev/null +++ b/scripts/sdk-coin-generator-v2/templates/generic-l1/test/unit/keyPair.ts.ejs @@ -0,0 +1,49 @@ +import { KeyPair } from '../../src/lib'; +import should from 'should'; + +describe('<%= coin %> KeyPair', function () { + describe('Key Generation', function () { + it('should generate a valid keypair without a seed', function () { + const keyPair = new KeyPair(); + const keys = keyPair.getKeys(); + + should.exist(keys.prv); + should.exist(keys.pub); + keys.prv!.should.be.a.String(); + keys.pub.should.be.a.String(); + }); + + it('should generate a valid keypair with a seed', function () { + const seed = Buffer.from('0'.repeat(64), 'hex'); + const keyPair = new KeyPair({ seed }); + const keys = keyPair.getKeys(); + + should.exist(keys.prv); + should.exist(keys.pub); + keys.prv!.should.be.a.String(); + keys.pub.should.be.a.String(); + }); + + it('should generate the same keypair with the same seed', function () { + const seed = Buffer.from('0'.repeat(64), 'hex'); + const keyPair1 = new KeyPair({ seed }); + const keyPair2 = new KeyPair({ seed }); + + const keys1 = keyPair1.getKeys(); + const keys2 = keyPair2.getKeys(); + + should.exist(keys1.prv); + should.exist(keys2.prv); + keys1.prv!.should.equal(keys2.prv!); + keys1.pub.should.equal(keys2.pub); + }); + + it('should generate an address from the keypair', function () { + const keyPair = new KeyPair(); + const address = keyPair.getAddress(); + + address.should.be.a.String(); + address.length.should.be.greaterThan(0); + }); + }); +}); diff --git a/scripts/sdk-coin-generator-v2/templates/generic-l1/test/unit/utils.ts.ejs b/scripts/sdk-coin-generator-v2/templates/generic-l1/test/unit/utils.ts.ejs new file mode 100644 index 0000000000..85584ffa41 --- /dev/null +++ b/scripts/sdk-coin-generator-v2/templates/generic-l1/test/unit/utils.ts.ejs @@ -0,0 +1,56 @@ +import should from 'should'; +import utils from '../../src/lib/utils'; + +describe('<%= coin %> Utils', function () { + describe('Address Validation', function () { + it('should validate a valid address', function () { + // TODO: Add valid address examples for <%= coin %> + const validAddress = 'validAddress123'; + utils.isValidAddress(validAddress).should.be.true(); + }); + + it('should invalidate an invalid address', function () { + const invalidAddress = 'invalid!@#$'; + utils.isValidAddress(invalidAddress).should.be.false(); + }); + + it('should invalidate an empty address', function () { + utils.isValidAddress('').should.be.false(); + }); + }); + + describe('Public Key Validation', function () { + it('should validate a valid public key', function () { + // TODO: Add valid public key examples for <%= coin %> + const validPubKey = '0'.repeat(64); + utils.isValidPublicKey(validPubKey).should.be.true(); + }); + + it('should invalidate an invalid public key', function () { + const invalidPubKey = 'notahexstring'; + utils.isValidPublicKey(invalidPubKey).should.be.false(); + }); + + it('should invalidate a public key with wrong length', function () { + const wrongLengthPubKey = '0'.repeat(32); + utils.isValidPublicKey(wrongLengthPubKey).should.be.false(); + }); + }); + + describe('Private Key Validation', function () { + it('should validate a valid private key', function () { + const validPrvKey = '0'.repeat(64); + utils.isValidPrivateKey(validPrvKey).should.be.true(); + }); + + it('should invalidate an invalid private key', function () { + const invalidPrvKey = 'notahexstring'; + utils.isValidPrivateKey(invalidPrvKey).should.be.false(); + }); + + it('should invalidate a private key with wrong length', function () { + const wrongLengthPrvKey = '0'.repeat(32); + utils.isValidPrivateKey(wrongLengthPrvKey).should.be.false(); + }); + }); +}); diff --git a/scripts/sdk-coin-generator-v2/tsconfig.json b/scripts/sdk-coin-generator-v2/tsconfig.json new file mode 100644 index 0000000000..35950bd440 --- /dev/null +++ b/scripts/sdk-coin-generator-v2/tsconfig.json @@ -0,0 +1,25 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "module": "commonjs", + "target": "ES2020", + "lib": ["ES2020"], + "moduleResolution": "node", + "esModuleInterop": true, + "skipLibCheck": true, + "strict": true, + "outDir": "./dist", + "rootDir": ".", + "declaration": true + }, + "include": [ + "index.ts", + "core/**/*.ts", + "plugins/**/*.ts" + ], + "exclude": [ + "templates/**/*", + "node_modules", + "dist" + ] +} diff --git a/yarn.lock b/yarn.lock index 4f79808faa..83d7a16145 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1013,6 +1013,23 @@ resolved "https://registry.npmjs.org/@cbor-extract/cbor-extract-win32-x64/-/cbor-extract-win32-x64-2.2.0.tgz#4b3f07af047f984c082de34b116e765cb9af975f" integrity sha512-l2M+Z8DO2vbvADOBNLbbh9y5ST1RY5sqkWOg/58GkUPBYou/cuNZ68SGQ644f1CvZ8kcOxyZtw06+dxWHIoN/w== +"@clack/core@^0.3.3": + version "0.3.5" + resolved "https://registry.npmjs.org/@clack/core/-/core-0.3.5.tgz#3e1454c83a329353cc3a6ff8491e4284d49565bb" + integrity sha512-5cfhQNH+1VQ2xLQlmzXMqUoiaH0lRBq9/CLW9lTyMbuKLC3+xEK01tHVvyut++mLOn5urSHmkm6I0Lg9MaJSTQ== + dependencies: + picocolors "^1.0.0" + sisteransi "^1.0.5" + +"@clack/prompts@^0.7.0": + version "0.7.0" + resolved "https://registry.npmjs.org/@clack/prompts/-/prompts-0.7.0.tgz#6aaef48ea803d91cce12bc80811cfcb8de2e75ea" + integrity sha512-0MhX9/B4iL6Re04jPrttDm+BsP8y6mS7byuv0BvXgdXhbV5PdlsHt55dvNsuBCPZ7xq1oTAOOuotR9NFbQyMSA== + dependencies: + "@clack/core" "^0.3.3" + picocolors "^1.0.0" + sisteransi "^1.0.5" + "@colors/colors@1.5.0": version "1.5.0" resolved "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz" @@ -18977,6 +18994,11 @@ sirv@^2.0.3: mrmime "^2.0.0" totalist "^3.0.0" +sisteransi@^1.0.5: + version "1.0.5" + resolved "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" + integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== + sjcl@1.0.8, sjcl@^1.0.6: version "1.0.8" resolved "https://registry.npmjs.org/sjcl/-/sjcl-1.0.8.tgz"