diff --git a/modules/sdk-coin-tempo/.eslintignore b/modules/sdk-coin-tempo/.eslintignore new file mode 100644 index 0000000000..de4d1f007d --- /dev/null +++ b/modules/sdk-coin-tempo/.eslintignore @@ -0,0 +1,2 @@ +dist +node_modules diff --git a/modules/sdk-coin-tempo/.gitignore b/modules/sdk-coin-tempo/.gitignore new file mode 100644 index 0000000000..d62464f3e6 --- /dev/null +++ b/modules/sdk-coin-tempo/.gitignore @@ -0,0 +1,3 @@ +/dist +/node_modules +/.nyc_output diff --git a/modules/sdk-coin-tempo/.mocharc.yml b/modules/sdk-coin-tempo/.mocharc.yml new file mode 100644 index 0000000000..f499ec0a83 --- /dev/null +++ b/modules/sdk-coin-tempo/.mocharc.yml @@ -0,0 +1,8 @@ +require: 'tsx' +timeout: '60000' +reporter: 'min' +reporter-option: + - 'cdn=true' + - 'json=false' +exit: true +spec: ['test/unit/**/*.ts'] diff --git a/modules/sdk-coin-tempo/.npmignore b/modules/sdk-coin-tempo/.npmignore new file mode 100644 index 0000000000..85677ec29f --- /dev/null +++ b/modules/sdk-coin-tempo/.npmignore @@ -0,0 +1,10 @@ +**/*.ts +!**/*.d.ts +src +test +tsconfig.json +tslint.json +.gitignore +.eslintignore +.mocharc.yml +.prettierignore diff --git a/modules/sdk-coin-tempo/.prettierignore b/modules/sdk-coin-tempo/.prettierignore new file mode 100644 index 0000000000..de4d1f007d --- /dev/null +++ b/modules/sdk-coin-tempo/.prettierignore @@ -0,0 +1,2 @@ +dist +node_modules diff --git a/modules/sdk-coin-tempo/.prettierrc.yml b/modules/sdk-coin-tempo/.prettierrc.yml new file mode 100644 index 0000000000..5198f5e019 --- /dev/null +++ b/modules/sdk-coin-tempo/.prettierrc.yml @@ -0,0 +1,3 @@ +printWidth: 120 +singleQuote: true +trailingComma: es5 diff --git a/modules/sdk-coin-tempo/README.md b/modules/sdk-coin-tempo/README.md new file mode 100644 index 0000000000..7f191bb8b7 --- /dev/null +++ b/modules/sdk-coin-tempo/README.md @@ -0,0 +1,30 @@ +# BitGo sdk-coin-tempo + +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-tempo`. + +```shell +npm i @bitgo/sdk-api @bitgo/sdk-coin-tempo +``` + +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 { Tempo } from '@bitgo/sdk-coin-tempo'; + +const sdk = new BitGoAPI(); + +sdk.register('tempo', Tempo.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/modules/sdk-coin-tempo/package.json b/modules/sdk-coin-tempo/package.json new file mode 100644 index 0000000000..9cb9e1b608 --- /dev/null +++ b/modules/sdk-coin-tempo/package.json @@ -0,0 +1,53 @@ +{ + "name": "@bitgo/sdk-coin-tempo", + "version": "1.0.0", + "description": "BitGo SDK coin library for Tempo", + "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-tempo" + }, + "lint-staged": { + "*.{js,ts}": [ + "yarn prettier --write", + "yarn eslint --fix" + ] + }, + "publishConfig": { + "access": "public" + }, + "nyc": { + "extension": [ + ".ts" + ] + }, + "dependencies": { + "@bitgo/sdk-core": "^36.23.1", + "@bitgo/statics": "^58.16.1" + }, + "devDependencies": { + "@bitgo/sdk-api": "^1.71.9", + "@bitgo/sdk-test": "^9.1.17" + }, + "files": [ + "dist" + ] +} diff --git a/modules/sdk-coin-tempo/src/index.ts b/modules/sdk-coin-tempo/src/index.ts new file mode 100644 index 0000000000..12f3ca2d0e --- /dev/null +++ b/modules/sdk-coin-tempo/src/index.ts @@ -0,0 +1,4 @@ +export * from './lib'; +export * from './tempo'; +export * from './ttempo'; +export * from './register'; diff --git a/modules/sdk-coin-tempo/src/lib/constants.ts b/modules/sdk-coin-tempo/src/lib/constants.ts new file mode 100644 index 0000000000..704828cc00 --- /dev/null +++ b/modules/sdk-coin-tempo/src/lib/constants.ts @@ -0,0 +1,9 @@ +/** + * Constants for Tempo + */ + +export const MAINNET_COIN = 'tempo'; +export const TESTNET_COIN = 'ttempo'; + +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/modules/sdk-coin-tempo/src/lib/iface.ts b/modules/sdk-coin-tempo/src/lib/iface.ts new file mode 100644 index 0000000000..a3155f12da --- /dev/null +++ b/modules/sdk-coin-tempo/src/lib/iface.ts @@ -0,0 +1,18 @@ +/** + * Interfaces for Tempo + */ + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface TransactionData { + // TODO: Define transaction data structure +} + +export interface TransactionOutput { + address: string; + amount: string; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface TransactionInput { + // TODO: Define transaction input structure +} diff --git a/modules/sdk-coin-tempo/src/lib/index.ts b/modules/sdk-coin-tempo/src/lib/index.ts new file mode 100644 index 0000000000..f97b3b961e --- /dev/null +++ b/modules/sdk-coin-tempo/src/lib/index.ts @@ -0,0 +1,4 @@ +export * from './keyPair'; +export * from './utils'; +export * from './constants'; +export * from './iface'; diff --git a/modules/sdk-coin-tempo/src/lib/keyPair.ts b/modules/sdk-coin-tempo/src/lib/keyPair.ts new file mode 100644 index 0000000000..062fd3d4fb --- /dev/null +++ b/modules/sdk-coin-tempo/src/lib/keyPair.ts @@ -0,0 +1,67 @@ +import { DefaultKeys, isPrivateKey, isPublicKey, isSeed, KeyPairOptions } from '@bitgo/sdk-core'; +import * as crypto from 'crypto'; + +/** + * Tempo 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 Tempo + // 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/modules/sdk-coin-tempo/src/lib/utils.ts b/modules/sdk-coin-tempo/src/lib/utils.ts new file mode 100644 index 0000000000..88a11d4967 --- /dev/null +++ b/modules/sdk-coin-tempo/src/lib/utils.ts @@ -0,0 +1,40 @@ +import { VALID_ADDRESS_REGEX, VALID_PUBLIC_KEY_REGEX } from './constants'; + +/** + * Utility functions for Tempo + */ + +/** + * Check if the address is valid + * @param address + */ +export function isValidAddress(address: string): boolean { + // TODO: Implement proper address validation for Tempo + 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 Tempo + 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 Tempo + return privateKey.length === 64; +} + +const utils = { + isValidAddress, + isValidPublicKey, + isValidPrivateKey, +}; + +export default utils; diff --git a/modules/sdk-coin-tempo/src/register.ts b/modules/sdk-coin-tempo/src/register.ts new file mode 100644 index 0000000000..244c87bfd3 --- /dev/null +++ b/modules/sdk-coin-tempo/src/register.ts @@ -0,0 +1,8 @@ +import { BitGoBase } from '@bitgo/sdk-core'; +import { Tempo } from './tempo'; +import { Ttempo } from './ttempo'; + +export const register = (sdk: BitGoBase): void => { + sdk.register('tempo', Tempo.createInstance); + sdk.register('ttempo', Ttempo.createInstance); +}; diff --git a/modules/sdk-coin-tempo/src/tempo.ts b/modules/sdk-coin-tempo/src/tempo.ts new file mode 100644 index 0000000000..ecf93464c3 --- /dev/null +++ b/modules/sdk-coin-tempo/src/tempo.ts @@ -0,0 +1,157 @@ +import { + AuditDecryptedKeyParams, + BaseCoin, + BitGoBase, + KeyPair, + ParsedTransaction, + ParseTransactionOptions, + SignedTransaction, + SignTransactionOptions, + VerifyAddressOptions, + VerifyTransactionOptions, + TransactionExplanation, +} from '@bitgo/sdk-core'; +import { BaseCoin as StaticsBaseCoin } from '@bitgo/statics'; +import { KeyPair as TempoKeyPair } from './lib/keyPair'; +import utils from './lib/utils'; + +export class Tempo 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 Tempo(bitgo, staticsCoin); + } + + /** + * Factor between the coin's base unit and its smallest subdivision + */ + public getBaseFactor(): number { + return 1e18; + } + + public getChain(): string { + return 'tempo'; + } + + public getFamily(): string { + return 'tempo'; + } + + public getFullName(): string { + return 'Tempo'; + } + + /** + * 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 TempoKeyPair({ seed }) : new TempoKeyPair(); + 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/modules/sdk-coin-tempo/src/ttempo.ts b/modules/sdk-coin-tempo/src/ttempo.ts new file mode 100644 index 0000000000..8ea3107408 --- /dev/null +++ b/modules/sdk-coin-tempo/src/ttempo.ts @@ -0,0 +1,21 @@ +import { BaseCoin, BitGoBase } from '@bitgo/sdk-core'; +import { BaseCoin as StaticsBaseCoin } from '@bitgo/statics'; +import { Tempo } from './tempo'; + +export class Ttempo extends Tempo { + protected constructor(bitgo: BitGoBase, staticsCoin?: Readonly) { + super(bitgo, staticsCoin); + } + + static createInstance(bitgo: BitGoBase, staticsCoin?: Readonly): BaseCoin { + return new Ttempo(bitgo, staticsCoin); + } + + getChain(): string { + return 'ttempo'; + } + + getFullName(): string { + return 'Testnet Tempo'; + } +} diff --git a/modules/sdk-coin-tempo/test/integration/index.ts b/modules/sdk-coin-tempo/test/integration/index.ts new file mode 100644 index 0000000000..dbdc031c52 --- /dev/null +++ b/modules/sdk-coin-tempo/test/integration/index.ts @@ -0,0 +1,11 @@ +/** + * Integration tests for Tempo + * + * These tests require a running Tempo 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/modules/sdk-coin-tempo/test/unit/index.ts b/modules/sdk-coin-tempo/test/unit/index.ts new file mode 100644 index 0000000000..b0ffb2c381 --- /dev/null +++ b/modules/sdk-coin-tempo/test/unit/index.ts @@ -0,0 +1,63 @@ +import { Tempo } from '../../src/tempo'; +import { Ttempo } from '../../src/ttempo'; +import { BitGoAPI } from '@bitgo/sdk-api'; +import { TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test'; +import { BitGoBase } from '@bitgo/sdk-core'; +import { BaseCoin as StaticsBaseCoin } from '@bitgo/statics'; + +describe('Tempo Coin', function () { + let bitgo: TestBitGoAPI; + let basecoin; + + const registerCoin = (name: string, coinClass: typeof Tempo | typeof Ttempo): void => { + bitgo.safeRegister(name, (bitgo: BitGoBase) => { + // Create a mock statics coin + const mockStaticsCoin: Readonly = { + name, + fullName: name === 'tempo' ? 'Tempo' : 'Testnet Tempo', + network: { + type: name === 'tempo' ? 'mainnet' : 'testnet', + } as any, + features: [], + } as any; + return coinClass.createInstance(bitgo, mockStaticsCoin); + }); + }; + + before(function () { + bitgo = TestBitGo.decorate(BitGoAPI, { env: 'mock' }); + registerCoin('tempo', Tempo); + bitgo.initializeTestVars(); + basecoin = bitgo.coin('tempo'); + }); + + it('should instantiate the coin', function () { + const basecoin = bitgo.coin('tempo'); + basecoin.should.be.an.instanceof(Tempo); + }); + + it('should return the correct coin name', function () { + basecoin.getChain().should.equal('tempo'); + basecoin.getFullName().should.equal('Tempo'); + basecoin.getBaseFactor().should.equal(1e18); + }); + + describe('Testnet', function () { + let testnetBasecoin; + + before(function () { + registerCoin('ttempo', Ttempo); + testnetBasecoin = bitgo.coin('ttempo'); + }); + + it('should instantiate the testnet coin', function () { + testnetBasecoin.should.be.an.instanceof(Ttempo); + }); + + it('should return the correct testnet coin name', function () { + testnetBasecoin.getChain().should.equal('ttempo'); + testnetBasecoin.getFullName().should.equal('Testnet Tempo'); + testnetBasecoin.getBaseFactor().should.equal(1e18); + }); + }); +}); diff --git a/modules/sdk-coin-tempo/test/unit/keyPair.ts b/modules/sdk-coin-tempo/test/unit/keyPair.ts new file mode 100644 index 0000000000..30365d4094 --- /dev/null +++ b/modules/sdk-coin-tempo/test/unit/keyPair.ts @@ -0,0 +1,49 @@ +import { KeyPair } from '../../src/lib'; +import should from 'should'; + +describe('Tempo 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); + should(keys.prv).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); + should(keys.prv).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); + should(keys1.prv).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/modules/sdk-coin-tempo/test/unit/utils.ts b/modules/sdk-coin-tempo/test/unit/utils.ts new file mode 100644 index 0000000000..da670cf357 --- /dev/null +++ b/modules/sdk-coin-tempo/test/unit/utils.ts @@ -0,0 +1,55 @@ +import utils from '../../src/lib/utils'; + +describe('Tempo Utils', function () { + describe('Address Validation', function () { + it('should validate a valid address', function () { + // TODO: Add valid address examples for Tempo + 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 Tempo + 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/modules/sdk-coin-tempo/tsconfig.json b/modules/sdk-coin-tempo/tsconfig.json new file mode 100644 index 0000000000..b967e45713 --- /dev/null +++ b/modules/sdk-coin-tempo/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" + } + ] +}