import { describe, it, expect, beforeEach } from 'vitest';
import { StacksWalletClient } from './StacksWalletClient.js';
describe('StacksWalletClient', () => {
let wallet: StacksWalletClient;
// Real private key from Stacks devnet setup (deployer account)
const testPrivateKey = '0x753b7cc01a1a2e86221266a154af739463fce51219d97e4f856cd7200c3bd2a601';
beforeEach(() => {
wallet = new StacksWalletClient({
privateKey: testPrivateKey,
network: 'testnet'
});
});
describe('Wallet Initialization', () => {
it('should initialize with private key on testnet', () => {
expect(wallet.getAddress()).toBeDefined();
expect(wallet.getAddress()).toMatch(/^ST[0-9A-HJKMNP-Z]{39}$/); // Testnet address format (40 chars total including ST)
expect(wallet.getNetwork()).toBe('testnet');
expect(wallet.getPublicKey()).toBeDefined();
expect(wallet.getPrivateKey()).toBe('753b7cc01a1a2e86221266a154af739463fce51219d97e4f856cd7200c3bd2a601'); // Without 0x prefix
});
it('should initialize with private key on mainnet', () => {
const mainnetWallet = new StacksWalletClient({
privateKey: testPrivateKey,
network: 'mainnet'
});
expect(mainnetWallet.getAddress()).toBeDefined();
expect(mainnetWallet.getAddress()).toMatch(/^SP[0-9A-HJKMNP-Z]{39}$/); // Mainnet address format (40 chars total including SP)
expect(mainnetWallet.getNetwork()).toBe('mainnet');
});
it('should handle private key with 0x prefix', () => {
const walletWithPrefix = new StacksWalletClient({
privateKey: testPrivateKey, // Uses the real devnet key with 0x prefix
network: 'testnet'
});
expect(walletWithPrefix.getAddress()).toBeDefined();
expect(walletWithPrefix.getPrivateKey()).toBe('753b7cc01a1a2e86221266a154af739463fce51219d97e4f856cd7200c3bd2a601');
});
it('should handle private key without 0x prefix', () => {
const walletWithoutPrefix = new StacksWalletClient({
privateKey: '753b7cc01a1a2e86221266a154af739463fce51219d97e4f856cd7200c3bd2a601', // Real devnet key without 0x prefix
network: 'testnet'
});
expect(walletWithoutPrefix.getAddress()).toBeDefined();
expect(walletWithoutPrefix.getPrivateKey()).toBe('753b7cc01a1a2e86221266a154af739463fce51219d97e4f856cd7200c3bd2a601');
});
it('should throw error when neither privateKey nor mnemonic provided', () => {
expect(() => {
new StacksWalletClient({
network: 'testnet'
} as any);
}).toThrow('Either privateKey or mnemonic must be provided');
});
});
describe('Helper Methods', () => {
it('should generate correct explorer URLs for testnet', () => {
const txId = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef';
const explorerUrl = wallet.getExplorerUrl(txId);
expect(explorerUrl).toBe('https://explorer.hiro.so/txid/0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef?chain=testnet');
});
it('should generate correct explorer URLs for mainnet', () => {
const mainnetWallet = new StacksWalletClient({
privateKey: testPrivateKey,
network: 'mainnet'
});
const txId = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef';
const explorerUrl = mainnetWallet.getExplorerUrl(txId);
expect(explorerUrl).toBe('https://explorer.hiro.so/txid/0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef');
});
});
describe('Real API Integration', () => {
it('should retrieve real nonce from Stacks testnet API', async () => {
try {
const nonce = await wallet.getNonce();
// Real nonce should be a number >= 0
expect(typeof nonce).toBe('number');
expect(nonce).toBeGreaterThanOrEqual(0);
// Verify we're using the real devnet address
expect(wallet.getAddress()).toBe('ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM');
} catch (error) {
// If testnet is down, we should get a proper error message
expect(error).toBeInstanceOf(Error);
expect((error as Error).message).toContain('Failed to get nonce');
}
});
it('should retrieve real balance from Stacks testnet API', async () => {
try {
const balance = await wallet.getBalance();
// Real balance should have proper structure
expect(balance).toHaveProperty('stx');
expect(balance).toHaveProperty('locked');
expect(balance).toHaveProperty('raw');
// Values should be strings representing numbers
expect(typeof balance.stx).toBe('string');
expect(typeof balance.locked).toBe('string');
expect(typeof balance.raw.stx).toBe('string');
expect(typeof balance.raw.locked).toBe('string');
// Should be valid numbers
expect(parseFloat(balance.stx)).toBeGreaterThanOrEqual(0);
expect(parseFloat(balance.locked)).toBeGreaterThanOrEqual(0);
} catch (error) {
// If testnet is down, we should get a proper error message
expect(error).toBeInstanceOf(Error);
expect((error as Error).message).toContain('Failed to get balance');
}
});
it('should retrieve real account info from Stacks testnet API', async () => {
try {
const accountInfo = await wallet.getAccountInfo();
// Real account info should have proper structure
expect(accountInfo).toHaveProperty('address');
expect(accountInfo).toHaveProperty('balance');
expect(accountInfo).toHaveProperty('nonce');
expect(accountInfo).toHaveProperty('network');
// Address should match our wallet
expect(accountInfo.address).toBe('ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM');
expect(accountInfo.network).toBe('testnet');
// Nonce should be a valid number
expect(typeof accountInfo.nonce).toBe('number');
expect(accountInfo.nonce).toBeGreaterThanOrEqual(0);
} catch (error) {
// If testnet is down, we should get a proper error message
expect(error).toBeInstanceOf(Error);
expect((error as Error).message).toContain('Error fetching account info');
}
});
});
describe('Real Transaction Preparation', () => {
it('should properly build STX transfer to real testnet address', async () => {
// Test building a real STX transfer (without broadcasting)
// Using wallet_1 address from devnet as recipient
const recipient = 'ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5';
const amount = 0.001; // Small amount in STX
const memo = 'Real test transfer';
// Verify this would be a valid transfer structure
expect(recipient).toMatch(/^ST[0-9A-HJKMNP-Z]{39}$/);
expect(amount).toBeGreaterThan(0);
expect(memo).toBeDefined();
// Convert to microSTX
const microSTX = Math.floor(amount * 1000000);
expect(microSTX).toBe(1000); // 0.001 STX = 1000 microSTX
// Verify wallet is properly configured for real transaction
expect(wallet.getAddress()).toBe('ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM');
expect(wallet.getNetwork()).toBe('testnet');
expect(wallet.getPrivateKey()).toBe('753b7cc01a1a2e86221266a154af739463fce51219d97e4f856cd7200c3bd2a601');
});
it('should handle invalid recipient address properly', async () => {
// Test with genuinely invalid address format
const result = await wallet.transferSTX('invalid-address-format', 1.0, 'Test');
expect(result.success).toBe(false);
expect(result.error).toBeDefined();
expect(result.txId).toBe('');
});
});
describe('Real Contract Interactions', () => {
it('should call real read-only function on Stacks testnet', async () => {
try {
// Call a real contract function - get-symbol from a SIP-010 token contract
// Using ALEX token contract on testnet as an example
const result = await wallet.callReadOnlyFunction(
'ST1J3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5', // Real testnet contract address
'alex-token', // Real contract name
'get-symbol', // Real function that should exist
[]
);
// Should return a valid response structure
expect(result).toBeDefined();
// If the contract exists and function works, we should get a proper response
if (result.success !== false) {
expect(result).toHaveProperty('value');
}
} catch (error) {
// If contract doesn't exist or testnet is down, we should get proper error
expect(error).toBeInstanceOf(Error);
expect((error as Error).message).toContain('Read-only function call failed');
}
});
it('should call real POX contract on testnet', async () => {
try {
// Call the real POX contract to get stacking info
const result = await wallet.callReadOnlyFunction(
'ST000000000000000000002AMW42H', // POX contract address
'pox-4', // POX contract name
'get-pox-info', // Function to get POX info
[]
);
// Should return valid POX data structure
expect(result).toBeDefined();
// If successful, should have POX info structure
if (result.success !== false) {
expect(result).toHaveProperty('value');
}
} catch (error) {
// Even if it fails, should be a proper error
expect(error).toBeInstanceOf(Error);
expect((error as Error).message).toContain('Read-only function call failed');
}
});
});
describe('Stacking Operations', () => {
it('should return not implemented error for stacking', async () => {
const result = await wallet.stackSTX(100, 1, 'bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh');
expect(result.success).toBe(false);
expect(result.error).toContain('not yet implemented');
});
});
});