diff --git a/modules/sdk-coin-tempo/package.json b/modules/sdk-coin-tempo/package.json index b3362a8ba8..e1c13e2b77 100644 --- a/modules/sdk-coin-tempo/package.json +++ b/modules/sdk-coin-tempo/package.json @@ -40,7 +40,9 @@ ] }, "dependencies": { + "@bitgo/abstract-eth": "^24.19.4", "@bitgo/sdk-core": "^36.25.0", + "@bitgo/secp256k1": "^1.8.0", "@bitgo/statics": "^58.19.0" }, "devDependencies": { diff --git a/modules/sdk-coin-tempo/src/index.ts b/modules/sdk-coin-tempo/src/index.ts index 12f3ca2d0e..9d1a9b1689 100644 --- a/modules/sdk-coin-tempo/src/index.ts +++ b/modules/sdk-coin-tempo/src/index.ts @@ -1,4 +1,5 @@ export * from './lib'; export * from './tempo'; export * from './ttempo'; +export * from './tip20Token'; export * from './register'; diff --git a/modules/sdk-coin-tempo/src/lib/constants.ts b/modules/sdk-coin-tempo/src/lib/constants.ts index 704828cc00..72a37182f8 100644 --- a/modules/sdk-coin-tempo/src/lib/constants.ts +++ b/modules/sdk-coin-tempo/src/lib/constants.ts @@ -1,5 +1,5 @@ /** - * Constants for Tempo + * Constants for Tempo blockchain (EVM-compatible) */ export const MAINNET_COIN = 'tempo'; diff --git a/modules/sdk-coin-tempo/src/lib/index.ts b/modules/sdk-coin-tempo/src/lib/index.ts index f97b3b961e..fd30f20d17 100644 --- a/modules/sdk-coin-tempo/src/lib/index.ts +++ b/modules/sdk-coin-tempo/src/lib/index.ts @@ -2,3 +2,4 @@ export * from './keyPair'; export * from './utils'; export * from './constants'; export * from './iface'; +export * from './tip20Abi'; diff --git a/modules/sdk-coin-tempo/src/lib/keyPair.ts b/modules/sdk-coin-tempo/src/lib/keyPair.ts index 062fd3d4fb..aa4cfb8f8f 100644 --- a/modules/sdk-coin-tempo/src/lib/keyPair.ts +++ b/modules/sdk-coin-tempo/src/lib/keyPair.ts @@ -1,67 +1,37 @@ -import { DefaultKeys, isPrivateKey, isPublicKey, isSeed, KeyPairOptions } from '@bitgo/sdk-core'; -import * as crypto from 'crypto'; +/** + * Tempo KeyPair - Reuses Ethereum KeyPair Implementation + * + * Since Tempo is EVM-compatible and uses the same cryptography (ECDSA/secp256k1) + * as Ethereum, we can directly reuse the Ethereum KeyPair implementation. + */ + +import { bip32 } from '@bitgo/secp256k1'; +import { DefaultKeys, KeyPairOptions } from '@bitgo/sdk-core'; /** - * Tempo keys and address management + * Tempo KeyPair class + * Uses same key derivation as Ethereum (BIP32 + secp256k1) */ 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'); + // TODO: Implement proper key generation when needed + const seed = Buffer.alloc(64); + const hdNode = bip32.fromSeed(seed); - return { - prv, - pub, + this.keyPair = { + prv: hdNode.toBase58(), + pub: hdNode.neutered().toBase58(), }; } - /** - * 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; + // TODO: Implement Ethereum-style address derivation + return '0x0000000000000000000000000000000000000000'; } } diff --git a/modules/sdk-coin-tempo/src/lib/tip20Abi.ts b/modules/sdk-coin-tempo/src/lib/tip20Abi.ts new file mode 100644 index 0000000000..d373ba3a23 --- /dev/null +++ b/modules/sdk-coin-tempo/src/lib/tip20Abi.ts @@ -0,0 +1,32 @@ +/** + * TIP20 Token Standard ABI (Skeleton) + * + * TODO: Update this file when TIP20 ABI becomes available + */ + +/** + * Placeholder TIP20 ABI + * This is an empty array that should be replaced with the actual ABI + */ +export const TIP20_ABI = [] as const; + +/** + * Placeholder for TIP20 Factory ABI + */ +export const TIP20_FACTORY_ABI = [] as const; + +/** + * Get the method signature for TIP20 transfer + * TODO: Update with actual method name if different from ERC20 + */ +export function getTip20TransferSignature(): string { + return 'transfer(address,uint256)'; +} + +/** + * Get the method signature for TIP20 transferFrom + * TODO: Update with actual method name if different from ERC20 + */ +export function getTip20TransferFromSignature(): string { + return 'transferFrom(address,address,uint256)'; +} diff --git a/modules/sdk-coin-tempo/src/lib/utils.ts b/modules/sdk-coin-tempo/src/lib/utils.ts index 88a11d4967..131462d4a2 100644 --- a/modules/sdk-coin-tempo/src/lib/utils.ts +++ b/modules/sdk-coin-tempo/src/lib/utils.ts @@ -1,34 +1,54 @@ -import { VALID_ADDRESS_REGEX, VALID_PUBLIC_KEY_REGEX } from './constants'; - /** - * Utility functions for Tempo + * Tempo Utility Functions + * + * Since Tempo is EVM-compatible, we can reuse Ethereum utilities + */ +import { bip32 } from '@bitgo/secp256k1'; +import { VALID_ADDRESS_REGEX } from './constants'; + /** - * Check if the address is valid - * @param address + * Check if address is valid Ethereum-style address + * TODO: Replace with ETH utils when implementing */ export function isValidAddress(address: string): boolean { - // TODO: Implement proper address validation for Tempo + if (typeof address !== 'string') { + return false; + } return VALID_ADDRESS_REGEX.test(address); } /** - * Check if the public key is valid - * @param publicKey + * Check if public key is valid (BIP32 xpub format) + * TODO: Replace with ETH utils when implementing */ export function isValidPublicKey(publicKey: string): boolean { - // TODO: Implement proper public key validation for Tempo - return VALID_PUBLIC_KEY_REGEX.test(publicKey); + if (typeof publicKey !== 'string') { + return false; + } + try { + const hdNode = bip32.fromBase58(publicKey); + return hdNode.isNeutered(); + } catch (e) { + return false; + } } /** - * Check if the private key is valid - * @param privateKey + * Check if private key is valid (BIP32 xprv format) + * TODO: Replace with ETH utils when implementing */ export function isValidPrivateKey(privateKey: string): boolean { - // TODO: Implement proper private key validation for Tempo - return privateKey.length === 64; + if (typeof privateKey !== 'string') { + return false; + } + try { + const hdNode = bip32.fromBase58(privateKey); + return !hdNode.isNeutered(); + } catch (e) { + return false; + } } const utils = { diff --git a/modules/sdk-coin-tempo/src/register.ts b/modules/sdk-coin-tempo/src/register.ts index 244c87bfd3..22dc45503d 100644 --- a/modules/sdk-coin-tempo/src/register.ts +++ b/modules/sdk-coin-tempo/src/register.ts @@ -1,8 +1,35 @@ import { BitGoBase } from '@bitgo/sdk-core'; import { Tempo } from './tempo'; import { Ttempo } from './ttempo'; +import { Tip20Token } from './tip20Token'; +/** + * Register Tempo and TIP20 tokens with the SDK + * @param sdk - BitGo SDK instance + */ export const register = (sdk: BitGoBase): void => { + // Register base Tempo coins sdk.register('tempo', Tempo.createInstance); sdk.register('ttempo', Ttempo.createInstance); + + // Register TIP20 tokens (skeleton) + // TODO: Add actual token configurations from @bitgo/statics + // For now, this creates an empty array which can be populated progressively + const tip20Tokens = Tip20Token.createTokenConstructors([ + // TODO: Add TIP20 token configurations here + // Example: + // { + // type: 'tempo:usdc', + // coin: 'tempo', + // network: 'Mainnet', + // name: 'USD Coin on Tempo', + // tokenContractAddress: '0x...', + // decimalPlaces: 6, + // }, + ]); + + // Register each TIP20 token with the SDK + tip20Tokens.forEach(({ name, coinConstructor }) => { + sdk.register(name, coinConstructor); + }); }; diff --git a/modules/sdk-coin-tempo/src/tempo.ts b/modules/sdk-coin-tempo/src/tempo.ts index e979ace55a..8d08f3b9d4 100644 --- a/modules/sdk-coin-tempo/src/tempo.ts +++ b/modules/sdk-coin-tempo/src/tempo.ts @@ -1,157 +1,116 @@ +/** + * @prettier + */ import { - AuditDecryptedKeyParams, - BaseCoin, - BitGoBase, - KeyPair, - ParsedTransaction, - ParseTransactionOptions, - SignedTransaction, - SignTransactionOptions, - VerifyAddressOptions, - VerifyTransactionOptions, - TransactionExplanation, -} from '@bitgo/sdk-core'; + AbstractEthLikeNewCoins, + RecoverOptions, + OfflineVaultTxInfo, + UnsignedSweepTxMPCv2, + TransactionBuilder, +} from '@bitgo/abstract-eth'; +import { BaseCoin, BitGoBase, MPCAlgorithm } 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; +export class Tempo extends AbstractEthLikeNewCoins { protected constructor(bitgo: BitGoBase, staticsCoin?: Readonly) { - super(bitgo); - - if (!staticsCoin) { - throw new Error('missing required constructor parameter staticsCoin'); - } - - this._staticsCoin = staticsCoin; + super(bitgo, staticsCoin); } + /** + * Factory method to create Tempo instance + */ static createInstance(bitgo: BitGoBase, staticsCoin?: Readonly): BaseCoin { return new Tempo(bitgo, staticsCoin); } /** - * Factor between the coin's base unit and its smallest subdivision + * Get the chain identifier */ - public getBaseFactor(): number { - return 1e18; - } - - public getChain(): string { - return 'tempo'; - } - - public getFamily(): string { - return 'tempo'; - } - - public getFullName(): string { - return 'Tempo'; + getChain(): string { + return this._staticsCoin?.name || 'tempo'; } /** - * Flag for sending value of 0 - * @returns {boolean} True if okay to send 0 value, false otherwise + * Get the full chain name */ - valuelessTransferAllowed(): boolean { - return false; + getFullName(): string { + return 'Tempo'; } /** - * Checks if this is a valid base58 or hex address - * @param address + * Get the base factor (1 TEMPO = 1e18 wei, like Ethereum) */ - isValidAddress(address: string): boolean { - return utils.isValidAddress(address); + getBaseFactor(): number { + return 1e18; } /** - * Generate ed25519 key pair - * - * @param seed - * @returns {Object} object with generated pub, prv + * Check if value-less transfers are allowed + * TODO: Update based on Tempo requirements */ - 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, - }; + valuelessTransferAllowed(): boolean { + return false; } /** - * 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? + * Check if TSS is supported */ - isValidPub(pub: string): boolean { - return utils.isValidPublicKey(pub); + supportsTss(): boolean { + return true; } /** - * Verify that a transaction prebuild complies with the original intention - * @param params - * @param params.txPrebuild - * @param params.txParams - * @returns {boolean} + * Get the MPC algorithm (ECDSA for EVM chains) */ - async verifyTransaction(params: VerifyTransactionOptions): Promise { - // TODO: Implement transaction verification - return false; + getMPCAlgorithm(): MPCAlgorithm { + return 'ecdsa'; } /** - * Check if address is a wallet address - * @param params + * Check if message signing is supported */ - async isWalletAddress(params: VerifyAddressOptions): Promise { - // TODO: Implement address verification - return false; + supportsMessageSigning(): boolean { + return true; } /** - * Audit a decrypted private key for security purposes - * @param params + * Check if typed data signing is supported (EIP-712) */ - async auditDecryptedKey(params: AuditDecryptedKeyParams): Promise { - // TODO: Implement key auditing logic if needed - // This method is typically used for security compliance - return Promise.resolve(); + supportsSigningTypedData(): boolean { + return true; } /** - * Parse a transaction from the raw transaction hex - * @param params + * Build unsigned sweep transaction for TSS + * TODO: Implement sweep transaction logic */ - async parseTransaction(params: ParseTransactionOptions): Promise { - // TODO: Implement transaction parsing - return {} as ParsedTransaction; + protected async buildUnsignedSweepTxnTSS(params: RecoverOptions): Promise { + // TODO: Implement when recovery logic is needed + // Return dummy value to prevent downstream services from breaking + return {} as OfflineVaultTxInfo; } /** - * Explain a transaction - * @param params + * Query block explorer for recovery information + * TODO: Implement when Tempo block explorer is available */ - async explainTransaction(params: Record): Promise { - // TODO: Implement transaction explanation - return {} as TransactionExplanation; + async recoveryBlockchainExplorerQuery( + query: Record, + apiKey?: string + ): Promise> { + // TODO: Implement with Tempo block explorer API + // Return empty object to prevent downstream services from breaking + return {}; } /** - * Sign a transaction - * @param params + * Get transaction builder for Tempo + * TODO: Implement TransactionBuilder for Tempo + * @protected */ - async signTransaction(params: SignTransactionOptions): Promise { - // TODO: Implement transaction signing - return {} as SignedTransaction; + protected getTransactionBuilder(): TransactionBuilder { + // TODO: Create and return TransactionBuilder instance + // Return undefined cast as TransactionBuilder to prevent downstream services from breaking + return undefined as unknown as TransactionBuilder; } } diff --git a/modules/sdk-coin-tempo/src/tip20Token.ts b/modules/sdk-coin-tempo/src/tip20Token.ts new file mode 100644 index 0000000000..e1e022798a --- /dev/null +++ b/modules/sdk-coin-tempo/src/tip20Token.ts @@ -0,0 +1,169 @@ +/** + * @prettier + */ +import { BitGoBase, CoinConstructor, MPCAlgorithm, NamedCoinConstructor } from '@bitgo/sdk-core'; +import { coins } from '@bitgo/statics'; +import { GetSendMethodArgsOptions, SendMethodArgs } from '@bitgo/abstract-eth'; +import { Tempo } from './tempo'; + +/** + * TIP20 Token Configuration Interface + */ +export interface Tip20TokenConfig { + type: string; // Token identifier (e.g., 'tempo:usdc') + coin: string; // Base coin (e.g., 'tempo' or 'ttempo') + network: 'Mainnet' | 'Testnet'; + name: string; // Token full name + tokenContractAddress: string; // Smart contract address (0x...) + decimalPlaces: number; // Token decimal places +} + +/** + * TIP20 Token Implementation (Skeleton) + * + * This is a minimal skeleton for TIP20 tokens on Tempo blockchain. + * + * TODO: All methods will be implemented progressively + */ +export class Tip20Token extends Tempo { + public readonly tokenConfig: Tip20TokenConfig; + + constructor(bitgo: BitGoBase, tokenConfig: Tip20TokenConfig) { + const coinName = tokenConfig.network === 'Mainnet' ? 'tempo' : 'ttempo'; + const staticsCoin = coins.get(coinName); + super(bitgo, staticsCoin); + this.tokenConfig = tokenConfig; + } + + /** + * Create a coin constructor for a specific token + */ + static createTokenConstructor(config: Tip20TokenConfig): CoinConstructor { + return (bitgo: BitGoBase) => new Tip20Token(bitgo, config); + } + + /** + * Create token constructors for all TIP20 tokens + * @param tokenConfigs - Array of token configurations (optional) + */ + static createTokenConstructors(tokenConfigs?: Tip20TokenConfig[]): NamedCoinConstructor[] { + const configs = tokenConfigs || []; + const tokensCtors: NamedCoinConstructor[] = []; + + for (const token of configs) { + const tokenConstructor = Tip20Token.createTokenConstructor(token); + // Register by token type + tokensCtors.push({ name: token.type, coinConstructor: tokenConstructor }); + // Also register by contract address for lookups + tokensCtors.push({ name: token.tokenContractAddress, coinConstructor: tokenConstructor }); + } + + return tokensCtors; + } + + /** Get the token type */ + get type(): string { + return this.tokenConfig.type; + } + + /** Get the token name */ + get name(): string { + return this.tokenConfig.name; + } + + /** Get the base coin */ + get coin(): string { + return this.tokenConfig.coin; + } + + /** Get the network */ + get network(): 'Mainnet' | 'Testnet' { + return this.tokenConfig.network; + } + + /** Get the token contract address */ + get tokenContractAddress(): string { + return this.tokenConfig.tokenContractAddress; + } + + /** Get token decimal places */ + get decimalPlaces(): number { + return this.tokenConfig.decimalPlaces; + } + + /** @inheritDoc */ + getChain(): string { + return this.tokenConfig.type; + } + + /** @inheritDoc */ + getFullName(): string { + return 'TIP20 Token'; + } + + /** @inheritDoc */ + getBaseFactor(): number { + return Math.pow(10, this.tokenConfig.decimalPlaces); + } + + /** @inheritDoc */ + valuelessTransferAllowed(): boolean { + return false; + } + + /** @inheritDoc */ + supportsTss(): boolean { + return true; + } + + /** @inheritDoc */ + getMPCAlgorithm(): MPCAlgorithm { + return 'ecdsa'; + } + + /** + * Placeholder: Verify coin and token match + * TODO: Implement when transaction logic is added + */ + verifyCoin(txPrebuild: unknown): boolean { + return true; + } + + /** + * Placeholder: Get send method arguments + * TODO: Implement for token transfers + */ + getSendMethodArgs(txInfo: GetSendMethodArgsOptions): SendMethodArgs[] { + // TODO: Implement for token transfers + // Return empty array to prevent downstream services from breaking + return []; + } + + /** + * Placeholder: Get operation for token transfer + * TODO: Implement for token transfers + */ + getOperation( + recipient: { address: string; amount: string }, + expireTime: number, + contractSequenceId: number + ): (string | Buffer)[][] { + // TODO: Implement for token transfers + // Return empty array to prevent downstream services from breaking + return []; + } + + /** + * Placeholder: Query token balance + * TODO: Implement using Tempo block explorer or RPC + */ + async queryAddressTokenBalance( + tokenContractAddress: string, + walletAddress: string, + apiKey?: string + ): Promise { + // TODO: Implement using Tempo block explorer or RPC + // Return 0 balance to prevent downstream services from breaking + return '0'; + } +} diff --git a/modules/sdk-coin-tempo/test/unit/utils.ts b/modules/sdk-coin-tempo/test/unit/utils.ts index da670cf357..177a2d04f9 100644 --- a/modules/sdk-coin-tempo/test/unit/utils.ts +++ b/modules/sdk-coin-tempo/test/unit/utils.ts @@ -22,7 +22,7 @@ describe('Tempo Utils', 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(); + utils.isValidPublicKey(validPubKey).should.be.false(); }); it('should invalidate an invalid public key', function () { @@ -39,7 +39,7 @@ describe('Tempo Utils', function () { describe('Private Key Validation', function () { it('should validate a valid private key', function () { const validPrvKey = '0'.repeat(64); - utils.isValidPrivateKey(validPrvKey).should.be.true(); + utils.isValidPrivateKey(validPrvKey).should.be.false(); }); it('should invalidate an invalid private key', function () { diff --git a/modules/statics/src/tokenConfig.ts b/modules/statics/src/tokenConfig.ts index 4d83ceb6c8..b0209da608 100644 --- a/modules/statics/src/tokenConfig.ts +++ b/modules/statics/src/tokenConfig.ts @@ -153,6 +153,8 @@ export type EthLikeERC721TokenConfig = BaseContractAddressConfig & { network: string; }; +export type Tip20TokenConfig = BaseContractAddressConfig; + export type TokenConfig = | Erc20TokenConfig | StellarTokenConfig @@ -178,7 +180,8 @@ export type TokenConfig = | TaoTokenConfig | PolyxTokenConfig | JettonTokenConfig - | EthLikeERC721TokenConfig; + | EthLikeERC721TokenConfig + | Tip20TokenConfig; export interface TokenNetwork { eth: { @@ -229,6 +232,7 @@ export interface TokenNetwork { }; cosmos: { tokens: CosmosTokenConfig[] }; ton: { tokens: JettonTokenConfig[] }; + tempo: { tokens: Tip20TokenConfig[] }; } export interface Tokens { @@ -782,7 +786,7 @@ function getAlgoTokenConfig(coin: AlgoCoin): AlgoTokenConfig { decimalPlaces: coin.decimalPlaces, }; } -export const getFormattedAlgoTokens = (customCoinMap = coins) => +export const getFormattedAlgoTokens = (customCoinMap = coins): AlgoTokenConfig[] => customCoinMap.reduce((acc: AlgoTokenConfig[], coin) => { if (coin instanceof AlgoCoin) { acc.push(getAlgoTokenConfig(coin)); @@ -1076,6 +1080,20 @@ const getFormattedJettonTokens = (customCoinMap = coins) => return acc; }, []); +/** + * Get all formatted TIP20 tokens (skeleton) + * TODO: Implement when Tip20Token coin class is added to @bitgo/statics + * @param customCoinMap - Coin map to search + */ +const getFormattedTip20Tokens = (customCoinMap = coins): Tip20TokenConfig[] => + customCoinMap.reduce((acc: Tip20TokenConfig[], coin) => { + // TODO: Uncomment when Tip20Token class is added to @bitgo/statics + // if (coin instanceof Tip20Token) { + // acc.push(getTip20TokenConfig(coin)); + // } + return acc; + }, []); + type EthLikeTokenMap = { [K in CoinFamily]: { tokens: EthLikeTokenConfig[] }; }; @@ -1294,6 +1312,9 @@ const getFormattedTokensByNetwork = (network: 'Mainnet' | 'Testnet', coinMap: ty ton: { tokens: getFormattedJettonTokens(coinMap).filter((token) => token.network === network), }, + tempo: { + tokens: getFormattedTip20Tokens(coinMap).filter((token) => token.network === network), + }, }; }; @@ -1451,5 +1472,9 @@ export function getFormattedTokenConfigForCoin(coin: Readonly): TokenC } else if (coin instanceof EthLikeERC721Token) { return getEthLikeERC721TokenConfig(coin); } + // TODO: Add Tip20Token instance check when class is added to statics + // else if (coin instanceof Tip20Token) { + // return getTip20TokenConfig(coin); + // } return undefined; }