Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/accounts/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { getMinimumReserve, generateKeypair } from './keypair';
export { generateKeypair, getMinimumReserve } from './keypair';
export { AccountManager } from './manager';
96 changes: 96 additions & 0 deletions src/accounts/manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { AccountInfo, Signer, Thresholds } from '../types/network';
import { ValidationError } from '../utils/errors';

export class AccountManager {
/**
* Configure a multisig setup for an account
* @param accountId The account to configure
* @param signers Array of signers with their weights
* @param thresholds Threshold settings for low, medium, high operations
* @param masterSecretKey Secret key of the account (used only for setup)
* @returns Promise resolving to the updated account info
*/
async configureMultisig(
accountId: string,
signers: Signer[],
thresholds: Thresholds,
masterSecretKey: string,
): Promise<AccountInfo> {
// Validate inputs
if (!accountId || !signers || !thresholds || !masterSecretKey) {
throw new ValidationError('input', 'All parameters are required for multisig configuration');
}

if (signers.length < 2) {
throw new ValidationError('signers', 'At least 2 signers required for multisig');
}

if (thresholds.low < 0 || thresholds.medium < 0 || thresholds.high < 0) {
throw new ValidationError('thresholds', 'Thresholds must be non-negative');
}

// TODO: Implement actual Stellar transaction to configure multisig
// This would involve:
// 1. Load the account from Horizon
// 2. Build a setOptions transaction with signers and thresholds
// 3. Sign with master key
// 4. Submit to Horizon
// 5. Wait for confirmation

// For now, return mock implementation
const mockAccountInfo: AccountInfo = {
accountId,
balance: '10.0000000',
signers: signers.filter((s) => s.publicKey !== accountId), // Remove master key
thresholds,
sequenceNumber: '123456789',
exists: true,
};

return mockAccountInfo;
}

/**
* Create a new account on the Stellar network
* @param masterSecretKey Secret key to fund the new account
* @param startingBalance Initial XLM balance for the new account
* @returns Promise resolving to the new account's keypair
*/
async createAccount(
_masterSecretKey: string,
_startingBalance: string,
): Promise<{ publicKey: string; secretKey: string }> {
// TODO: Implement actual Stellar account creation
// This would involve:
// 1. Generate new keypair
// 2. Build createAccount transaction
// 3. Sign with master key
// 4. Submit to Horizon

// Mock implementation with unique keys
const timestamp = Date.now().toString();
const randomSuffix = Math.random().toString(36).substring(2, 8);

return {
publicKey: 'GD' + timestamp + randomSuffix.padEnd(54 - timestamp.length, 'A'),
secretKey: 'S' + timestamp + randomSuffix.padEnd(55 - timestamp.length, 'B'),
};
}

/**
* Get account information from Horizon
* @param accountId The account ID to query
* @returns Promise resolving to account info
*/
async getAccount(accountId: string): Promise<AccountInfo> {
// TODO: Implement actual Horizon API call
return {
accountId,
balance: '0.0000000',
signers: [],
thresholds: { low: 0, medium: 0, high: 0 },
sequenceNumber: '0',
exists: false,
};
}
}
145 changes: 145 additions & 0 deletions tests/unit/accounts/manager.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { AccountManager } from '../../../src/accounts/manager';
import type { Signer, Thresholds } from '../../../src/types/network';
import { ValidationError } from '../../../src/utils/errors';

describe('AccountManager', () => {
let accountManager: AccountManager;

beforeEach(() => {
accountManager = new AccountManager();
});

describe('configureMultisig', () => {
const mockAccountId = 'GTEST1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567';
const mockMasterSecretKey = 'SMASTER1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890';

const validSigners = [
{ publicKey: 'GADOPTER1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ123', weight: 1 },
{ publicKey: 'GOWNER1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ12345', weight: 1 },
{ publicKey: 'GPLATFORM1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ12', weight: 1 },
];

const validThresholds = { low: 0, medium: 2, high: 2 };

it('should throw ValidationError when accountId is missing', async () => {
await expect(
accountManager.configureMultisig('', validSigners, validThresholds, mockMasterSecretKey),
).rejects.toThrow(ValidationError);
});

it('should throw ValidationError when signers is missing', async () => {
await expect(
accountManager.configureMultisig(
mockAccountId,
undefined as unknown as Signer[],
validThresholds,
mockMasterSecretKey,
),
).rejects.toThrow(ValidationError);
});

it('should throw ValidationError when thresholds is missing', async () => {
await expect(
accountManager.configureMultisig(
mockAccountId,
validSigners,
undefined as unknown as Thresholds,
mockMasterSecretKey,
),
).rejects.toThrow(ValidationError);
});

it('should throw ValidationError when masterSecretKey is missing', async () => {
await expect(
accountManager.configureMultisig(mockAccountId, validSigners, validThresholds, ''),
).rejects.toThrow(ValidationError);
});

it('should throw ValidationError when less than 2 signers provided', async () => {
const singleSigner = [validSigners[0]];

await expect(
accountManager.configureMultisig(
mockAccountId,
singleSigner,
validThresholds,
mockMasterSecretKey,
),
).rejects.toThrow(ValidationError);
});

it('should throw ValidationError when thresholds are negative', async () => {
const invalidThresholds = { low: -1, medium: 2, high: 2 };

await expect(
accountManager.configureMultisig(
mockAccountId,
validSigners,
invalidThresholds,
mockMasterSecretKey,
),
).rejects.toThrow(ValidationError);
});

it('should return account info with filtered signers (removing master key)', async () => {
const result = await accountManager.configureMultisig(
mockAccountId,
validSigners,
validThresholds,
mockMasterSecretKey,
);

expect(result.accountId).toBe(mockAccountId);
expect(result.signers).toEqual(validSigners);
expect(result.thresholds).toEqual(validThresholds);
expect(result.exists).toBe(true);
expect(result.balance).toBe('10.0000000');
expect(result.sequenceNumber).toBe('123456789');
});

it('should handle signers that include the account ID (master key)', async () => {
const signersWithMaster = [
...validSigners,
{ publicKey: mockAccountId, weight: 1 }, // Master key included
];

const result = await accountManager.configureMultisig(
mockAccountId,
signersWithMaster,
validThresholds,
mockMasterSecretKey,
);

// Master key should be filtered out
expect(result.signers).toEqual(validSigners);
expect(result.signers).not.toContainEqual({ publicKey: mockAccountId, weight: 1 });
});
});

describe('createAccount', () => {
it('should return a new keypair', async () => {
const result = await accountManager.createAccount('SMASTER123', '2.0000000');

expect(result).toHaveProperty('publicKey');
expect(result).toHaveProperty('secretKey');
expect(result.publicKey).toMatch(/^GD/); // Stellar public keys start with GD
expect(result.secretKey).toMatch(/^S/); // Stellar secret keys start with S
expect(result.publicKey).toHaveLength(56); // Stellar public keys are 56 characters
});
});

describe('getAccount', () => {
it('should return account info for non-existent account', async () => {
const accountId = 'GNONEXISTENT1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ1';

const result = await accountManager.getAccount(accountId);

expect(result.accountId).toBe(accountId);
expect(result.exists).toBe(false);
expect(result.balance).toBe('0.0000000');
expect(result.signers).toEqual([]);
expect(result.thresholds).toEqual({ low: 0, medium: 0, high: 0 });
expect(result.sequenceNumber).toBe('0');
});
});
});
Loading
Loading