From 6d71b4a7285de1da0bb7a3de1751324ec1a4bec7 Mon Sep 17 00:00:00 2001 From: Sadeequ <70214653+Sadeequ@users.noreply.github.com> Date: Wed, 26 Aug 2026 12:33:52 +0000 Subject: [PATCH 1/2] all major fix for the CLI done --- cli/src/index.ts | 47 +++++++- cli/src/schema/bc-forge.schema.json | 5 + cli/src/utils/config-parser.ts | 1 + contracts/token/src/lib.rs | 47 ++++++++ sdk/src/client.ts | 161 ++++++++++++++++++++++++++++ sdk/src/index.ts | 2 +- 6 files changed, 258 insertions(+), 5 deletions(-) diff --git a/cli/src/index.ts b/cli/src/index.ts index 498b5306..a8ec228c 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -66,12 +66,14 @@ configCmd .option('--name ', 'Token name', 'MyToken') .option('--symbol ', 'Token symbol', 'MTK') .option('--decimals ', 'Token decimals', '7') + .option('--admin ', 'Admin Stellar G-address') .action((options) => { const templateConfig: BcForgeConfig = { version: '1.0.0', name: options.name, symbol: options.symbol, decimals: parseInt(options.decimals, 10), + admin: options.admin, network: 'testnet', rpcUrl: 'https://soroban-testnet.stellar.org', networkPassphrase: 'Test SDF Network ; September 2015' @@ -113,6 +115,8 @@ program .option('--decimals ', 'Decimal places') .option('--name ', 'Token name') .option('--symbol ', 'Token symbol') + .option('--pauser
', 'Multisig address to grant Pauser role to') + .option('--verify', 'Verify on-chain state after initialization', false) .action(async (options) => { try { const fileConfig = loadConfigFile().config; @@ -120,6 +124,8 @@ program const decimals = options.decimals ? parseInt(options.decimals, 10) : fileConfig?.decimals || 7; const name = options.name || fileConfig?.name; const symbol = options.symbol || fileConfig?.symbol; + const pauser = options.pauser || fileConfig?.pauser; + const verify = options.verify || false; if (!admin || !name || !symbol) { throw new Error('Missing required options: admin, name, symbol must be specified or present in .bc-forge.json'); @@ -133,14 +139,47 @@ program logger.warn('Initializing contract...'); logger.debug(`Init params: name=${name}, symbol=${symbol}, decimals=${decimals}, admin=${admin}`); - + const result = await client.initialize(admin, decimals, name, symbol, source); - if (result.success) { - logger.success(`Contract initialized. TX: ${result.hash}`); - } else { + if (!result.success) { logger.error(`Initialization failed. TX: ${result.hash}`); process.exitCode = 1; + return; + } + logger.success(`Contract initialized. TX: ${result.hash}`); + + if (pauser) { + logger.warn(`Granting Pauser role to ${pauser}...`); + logger.debug(`Pauser grant: admin=${admin}, pauser=${pauser}`); + const pauserResult = await client.grantPauser(pauser, source); + if (pauserResult.success) { + logger.success(`Pauser role granted. TX: ${pauserResult.hash}`); + } else { + logger.error(`Failed to grant Pauser role. TX: ${pauserResult.hash}`); + process.exitCode = 1; + return; + } + } + + if (verify) { + logger.warn('Verifying on-chain state...'); + const state = await client.verifyInitializedState(admin, name, symbol, decimals); + if (state.valid) { + logger.success('On-chain state verification passed'); + logger.info(` Admin: ${state.admin}`); + logger.info(` Name: ${state.name}`); + logger.info(` Symbol: ${state.symbol}`); + logger.info(` Decimals: ${state.decimals}`); + logger.info(` Total Supply: ${state.totalSupply}`); + if (pauser) { + logger.info(` Pauser role granted: ${state.pauserGranted}`); + } + } else { + logger.error('On-chain state verification failed:'); + state.errors.forEach(err => logger.error(` - ${err}`)); + process.exitCode = 1; + } } } catch (err: any) { logger.error(`Error: ${err.message}`); diff --git a/cli/src/schema/bc-forge.schema.json b/cli/src/schema/bc-forge.schema.json index 27a77430..1b3ce539 100644 --- a/cli/src/schema/bc-forge.schema.json +++ b/cli/src/schema/bc-forge.schema.json @@ -29,6 +29,11 @@ "pattern": "^G[A-Z2-7]{55}$", "description": "Stellar G-address of the contract admin" }, + "pauser": { + "type": "string", + "pattern": "^G[A-Z2-7]{55}$", + "description": "Stellar G-address to receive the Pauser role (multisig)" + }, "network": { "type": "string", "enum": ["mainnet", "testnet", "futurenet", "standalone", "custom"], diff --git a/cli/src/utils/config-parser.ts b/cli/src/utils/config-parser.ts index 5a92b3c3..4bfa4b3c 100644 --- a/cli/src/utils/config-parser.ts +++ b/cli/src/utils/config-parser.ts @@ -21,6 +21,7 @@ export interface BcForgeConfig { symbol: string; decimals?: number; admin?: string; + pauser?: string; network?: 'mainnet' | 'testnet' | 'futurenet' | 'standalone' | 'custom' | string; rpcUrl?: string; networkPassphrase?: string; diff --git a/contracts/token/src/lib.rs b/contracts/token/src/lib.rs index 8be794c3..371594f1 100644 --- a/contracts/token/src/lib.rs +++ b/contracts/token/src/lib.rs @@ -318,10 +318,12 @@ impl BcForgeToken { /// Initializes the token contract. /// /// Sets the admin address, decimals, name, and symbol. + /// Configures default rate limits for mint, transfer, transfer_from, burn, and burn_from operations. /// Emits the `init` event. Can only be called once. /// /// @notice Initializes the token contract with the given admin, decimals, name, and symbol. /// @dev This function can only be called once. Subsequent calls will revert with `AlreadyInitialized`. + /// Default rate limits are set to 1000 operations per 60-second window for each operation type. /// @param env The Soroban environment. /// @param admin_address The address to set as the contract admin. /// @param decimal The number of decimal places for the token. @@ -345,10 +347,55 @@ impl BcForgeToken { env.storage().instance().set(&DataKey::Symbol, &symbol); Self::write_supply(&env, 0); Self::write_max_supply(&env, i128::MAX); + + Self::set_default_rate_limits(&env); + events::emit_initialized(&env, &admin_address, decimal, &name, &symbol); Ok(()) } + /// Sets default rate limits for all operation types during initialization. + /// + /// Configures global rate limits with sensible defaults: + /// - 1000 operations per 60-second window for each operation type. + /// + /// @param env The Soroban environment. + fn set_default_rate_limits(env: &Env) { + let default_limit: u64 = 1000; + let default_window: u64 = 60; + + bc_forge_rate_limit::BcForgeRateLimit::internal_set_global_rate_limit( + env, + &soroban_sdk::String::from_str(env, crate::rate_limit::OPERATION_MINT), + default_limit, + default_window, + ); + bc_forge_rate_limit::BcForgeRateLimit::internal_set_global_rate_limit( + env, + &soroban_sdk::String::from_str(env, crate::rate_limit::OPERATION_TRANSFER), + default_limit, + default_window, + ); + bc_forge_rate_limit::BcForgeRateLimit::internal_set_global_rate_limit( + env, + &soroban_sdk::String::from_str(env, crate::rate_limit::OPERATION_TRANSFER_FROM), + default_limit, + default_window, + ); + bc_forge_rate_limit::BcForgeRateLimit::internal_set_global_rate_limit( + env, + &soroban_sdk::String::from_str(env, crate::rate_limit::OPERATION_BURN), + default_limit, + default_window, + ); + bc_forge_rate_limit::BcForgeRateLimit::internal_set_global_rate_limit( + env, + &soroban_sdk::String::from_str(env, crate::rate_limit::OPERATION_BURN_FROM), + default_limit, + default_window, + ); + } + /// Returns the admin address. /// /// @notice Returns the address of the contract admin. diff --git a/sdk/src/client.ts b/sdk/src/client.ts index 392ec5cc..acbf43a1 100644 --- a/sdk/src/client.ts +++ b/sdk/src/client.ts @@ -60,6 +60,26 @@ export interface BatchMintRecipient { amount: bigint; } +/** Result of on-chain state verification after initialization */ +export interface InitVerificationResult { + /** Whether all checks passed */ + valid: boolean; + /** Admin address from contract */ + admin?: string; + /** Token name from contract */ + name?: string; + /** Token symbol from contract */ + symbol?: string; + /** Token decimals from contract */ + decimals?: number; + /** Total token supply */ + totalSupply?: bigint; + /** Whether the Pauser role was granted to the expected address */ + pauserGranted?: boolean; + /** List of verification errors (empty if valid) */ + errors: string[]; +} + /** Role for role-based access control */ export enum Role { Admin = 'Admin', @@ -171,6 +191,94 @@ export class bcForgeClient { return scValToNative(result) as string; } + // ─── Initialization Verification ────────────────────────────────────────── + + /** + * Verify the on-chain state matches expected values after initialization. + * + * Queries the contract for its current state and compares against the + * expected values provided during initialization. + * + * @param expectedAdmin - The expected admin address + * @param expectedName - The expected token name + * @param expectedSymbol - The expected token symbol + * @param expectedDecimals - The expected number of decimals + * @param expectedPauser - Optional pauser address to verify role grant + * @returns Verification result with any mismatches + */ + async verifyInitializedState( + expectedAdmin: string, + expectedName: string, + expectedSymbol: string, + expectedDecimals: number, + expectedPauser?: string, + ): Promise { + const errors: string[] = []; + const result: InitVerificationResult = { valid: false, errors }; + + try { + const onChainAdmin = await this.getAdmin(); + result.admin = onChainAdmin; + if (onChainAdmin !== expectedAdmin) { + errors.push(`Admin mismatch: expected ${expectedAdmin}, got ${onChainAdmin}`); + } + } catch (err: any) { + errors.push(`Failed to query admin: ${err.message}`); + } + + try { + const onChainName = await this.getName(); + result.name = onChainName; + if (onChainName !== expectedName) { + errors.push(`Name mismatch: expected "${expectedName}", got "${onChainName}"`); + } + } catch (err: any) { + errors.push(`Failed to query name: ${err.message}`); + } + + try { + const onChainSymbol = await this.getSymbol(); + result.symbol = onChainSymbol; + if (onChainSymbol !== expectedSymbol) { + errors.push(`Symbol mismatch: expected "${expectedSymbol}", got "${onChainSymbol}"`); + } + } catch (err: any) { + errors.push(`Failed to query symbol: ${err.message}`); + } + + try { + const onChainDecimals = await this.getDecimals(); + result.decimals = onChainDecimals; + if (onChainDecimals !== expectedDecimals) { + errors.push(`Decimals mismatch: expected ${expectedDecimals}, got ${onChainDecimals}`); + } + } catch (err: any) { + errors.push(`Failed to query decimals: ${err.message}`); + } + + try { + const totalSupply = await this.getTotalSupply(); + result.totalSupply = totalSupply; + } catch (err: any) { + errors.push(`Failed to query total supply: ${err.message}`); + } + + if (expectedPauser) { + try { + const hasPauserRole = await this.hasRole(Role.Pauser, expectedPauser); + result.pauserGranted = hasPauserRole; + if (!hasPauserRole) { + errors.push(`Pauser role not granted to ${expectedPauser}`); + } + } catch (err: any) { + errors.push(`Failed to check Pauser role: ${err.message}`); + } + } + + result.valid = errors.length === 0; + return result; + } + // ─── Batch Queries ─────────────────────────────────────────────────────── /** @@ -828,6 +936,59 @@ export class bcForgeClient { ); } + /** + * Grant the Pauser role to an address. Admin-only. + * + * @param address - Address to grant the Pauser role to + * @param source - Admin keypair + */ + async grantPauser(address: string, source: Keypair): Promise { + return this.invokeContract( + 'grant_role', + [addressToScVal(source.publicKey()), nativeToScVal(Role.Pauser), addressToScVal(address)], + source, + ); + } + + /** + * Revoke the Pauser role from an address. Admin-only. + * + * @param address - Address to revoke the Pauser role from + * @param source - Admin keypair + */ + async revokePauser(address: string, source: Keypair): Promise { + return this.invokeContract( + 'revoke_role', + [addressToScVal(source.publicKey()), nativeToScVal(Role.Pauser), addressToScVal(address)], + source, + ); + } + + /** + * Get the contract admin address. + * + * @returns The admin address + */ + async getAdmin(): Promise { + const result = await this.queryContract('admin', []); + return scValToNative(result) as string; + } + + /** + * Check if an address holds a specific role. + * + * @param role - The role to check for + * @param address - The address to check + * @returns Whether the address holds the role + */ + async hasRole(role: Role, address: string): Promise { + const result = await this.queryContract('has_role', [ + nativeToScVal(role), + addressToScVal(address), + ]); + return scValToNative(result) as boolean; + } + // ─── Clawback / Regulatory ─────────────────────────────────────────────── /** diff --git a/sdk/src/index.ts b/sdk/src/index.ts index bd70cb9f..968490cf 100644 --- a/sdk/src/index.ts +++ b/sdk/src/index.ts @@ -19,7 +19,7 @@ */ export { bcForgeClient, Role } from './client'; -export type { BatchMintRecipient, bcForgeClientConfig, TransactionResult } from './client'; +export type { BatchMintRecipient, bcForgeClientConfig, TransactionResult, InitVerificationResult } from './client'; export { buildInvokeTransaction, submitTransaction, scValToNative } from './utils'; export { bcForgeEventType, decodeEvent, decodeDiagnosticEvent, subscribeEvents } from './events'; export type { bcForgeEvent, SubscriptionOptions } from './events'; From 13c93f7f7ee6fb0c18dc1be33dadfcc0bf413a99 Mon Sep 17 00:00:00 2001 From: Promise Raji Date: Mon, 31 Aug 2026 12:07:01 +0100 Subject: [PATCH 2/2] Keep token initialize from writing rate-limit slots and drop duplicate SDK methods. Co-authored-by: Cursor --- contracts/token/src/lib.rs | 44 -------------------------------------- sdk/src/client.ts | 25 ---------------------- 2 files changed, 69 deletions(-) diff --git a/contracts/token/src/lib.rs b/contracts/token/src/lib.rs index a6b0329d..327f4ef9 100644 --- a/contracts/token/src/lib.rs +++ b/contracts/token/src/lib.rs @@ -427,54 +427,10 @@ impl BcForgeToken { Self::write_supply(&env, 0); Self::write_max_supply(&env, i128::MAX); - Self::set_default_rate_limits(&env); - events::emit_initialized(&env, &admin_address, decimal, &name, &symbol); Ok(()) } - /// Sets default rate limits for all operation types during initialization. - /// - /// Configures global rate limits with sensible defaults: - /// - 1000 operations per 60-second window for each operation type. - /// - /// @param env The Soroban environment. - fn set_default_rate_limits(env: &Env) { - let default_limit: u64 = 1000; - let default_window: u64 = 60; - - bc_forge_rate_limit::BcForgeRateLimit::internal_set_global_rate_limit( - env, - &soroban_sdk::String::from_str(env, crate::rate_limit::OPERATION_MINT), - default_limit, - default_window, - ); - bc_forge_rate_limit::BcForgeRateLimit::internal_set_global_rate_limit( - env, - &soroban_sdk::String::from_str(env, crate::rate_limit::OPERATION_TRANSFER), - default_limit, - default_window, - ); - bc_forge_rate_limit::BcForgeRateLimit::internal_set_global_rate_limit( - env, - &soroban_sdk::String::from_str(env, crate::rate_limit::OPERATION_TRANSFER_FROM), - default_limit, - default_window, - ); - bc_forge_rate_limit::BcForgeRateLimit::internal_set_global_rate_limit( - env, - &soroban_sdk::String::from_str(env, crate::rate_limit::OPERATION_BURN), - default_limit, - default_window, - ); - bc_forge_rate_limit::BcForgeRateLimit::internal_set_global_rate_limit( - env, - &soroban_sdk::String::from_str(env, crate::rate_limit::OPERATION_BURN_FROM), - default_limit, - default_window, - ); - } - /// Returns the admin address. /// /// @notice Returns the address of the contract admin. diff --git a/sdk/src/client.ts b/sdk/src/client.ts index 982b9e69..2560ff3e 100644 --- a/sdk/src/client.ts +++ b/sdk/src/client.ts @@ -1175,31 +1175,6 @@ export class bcForgeClient { ); } - /** - * Get the contract admin address. - * - * @returns The admin address - */ - async getAdmin(): Promise { - const result = await this.queryContract('admin', []); - return scValToNative(result) as string; - } - - /** - * Check if an address holds a specific role. - * - * @param role - The role to check for - * @param address - The address to check - * @returns Whether the address holds the role - */ - async hasRole(role: Role, address: string): Promise { - const result = await this.queryContract('has_role', [ - nativeToScVal(role), - addressToScVal(address), - ]); - return scValToNative(result) as boolean; - } - // ─── Clawback / Regulatory ─────────────────────────────────────────────── /**