import { getUtxosIdsFromPsbts } from '@saturnbtcio/psbt'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { PoolErrorException, PoolErrorType } from '../../error/pool.error'; import { Wallet } from '../../wallet/wallet.dto'; import { validatePoolSdkData, validateSaturnConfigData, verifyUtxosInPsbtNotUsedAndStillOwnedByUser, } from '../validation'; const testUtxoIds = ['tx1:0', 'tx2:0']; vi.mock('@saturnbtcio/psbt', () => ({ getUtxosIdsFromPsbts: vi.fn(() => testUtxoIds), })); interface TestUtxo { txid: string; vout: number; value: bigint; } const createTestWallet = (utxos: TestUtxo[]): Wallet => ({ address: 'dummyWalletAddress', balance: 1000000n, utxos: utxos as any, }); describe('verifyUtxosInPsbtNotUsedAndStillOwnedByUser', () => { let wallet: Wallet; beforeEach(() => { wallet = createTestWallet([ { txid: 'tx1', vout: 0, value: 1000n }, { txid: 'tx2', vout: 0, value: 2000n }, ]); }); it('Returns UTXO IDs if all UTXOs in PSBT are still owned by the user', async () => { const testPsbt = 'dummyPSBTstring'; const result = await verifyUtxosInPsbtNotUsedAndStillOwnedByUser(testPsbt, [ wallet, ]); expect(result).toEqual(testUtxoIds); }); it('Throws an error if any UTXO from PSBT is not in the wallet', async () => { vi.mocked(getUtxosIdsFromPsbts).mockReturnValue(['tx1:0', 'tx3:0']); const testPsbt = 'dummyPSBTstring'; const result = verifyUtxosInPsbtNotUsedAndStillOwnedByUser(testPsbt, [ wallet, ]); await expect(result).rejects.toEqual( new PoolErrorException({ type: PoolErrorType.InvalidUtxo, message: 'Invalid utxo. Utxo is not in wallet', utxos: ['tx1:0', 'tx3:0'], }), ); }); }); const validBigInt = 1n; const invalidBigInt = 0n; const validNumber = 1; const invalidNumber = 0; const validPubkey = 'a'.repeat(64); const invalidPubkey = 'zzz'; const validPsbt = 'SGVsbG8='; const invalidPsbt = 'notbase64!'; const validFeeTier = 10000; const invalidFeeTier = 999; const validAddress = 'validAddress'; const invalidAddress = 'invalidAddress'; const dummyNetwork = { bech32: 'bc', pubKeyHash: 0, scriptHash: 5, wif: 128, }; vi.mock('@scure/btc-signer', async () => { const actual = await vi.importActual('@scure/btc-signer'); return { ...actual, Address: (network: any) => ({ decode: (addr: string) => { if (addr === 'validAddress') return { type: 'p2wpkh' }; return { type: 'unknown' }; }, encode: (decoded: any) => { return 'dummyEncodedAddress'; }, }), }; }); describe('validatePoolSdkData', () => { // Parameterized tests for each field in poolSdkValidation. it.each([ ['feeRate', validBigInt, invalidBigInt, PoolErrorType.InvalidFeeRate], [ 'liquidityAmount', validBigInt, invalidBigInt, PoolErrorType.InvalidFeeRate, ], [ 'collectionAmount', validBigInt, invalidBigInt, PoolErrorType.InvalidFeeRate, ], ['btcAmount', validBigInt, invalidBigInt, PoolErrorType.InvalidFeeRate], [ 'feeRateOracleAccount', validPubkey, invalidPubkey, PoolErrorType.InvalidPubkey, ], [ 'mempoolInfoOracleAccount', validPubkey, invalidPubkey, PoolErrorType.InvalidPubkey, ], ['programAccount', validPubkey, invalidPubkey, PoolErrorType.InvalidPubkey], ['poolId', validPubkey, invalidPubkey, PoolErrorType.InvalidPubkey], ['positionPubKey', validPubkey, invalidPubkey, PoolErrorType.InvalidPubkey], ['signedPsbt', validPsbt, invalidPsbt, PoolErrorType.InvalidPsbt], ['mergeUtxoPsbt', validPsbt, invalidPsbt, PoolErrorType.InvalidPsbt], ['splitRunePsbt', validPsbt, invalidPsbt, PoolErrorType.InvalidPsbt], ['amount0', validNumber, invalidNumber, PoolErrorType.InvalidNumericValue], ['amount1', validNumber, invalidNumber, PoolErrorType.InvalidNumericValue], ['amountIn', validNumber, invalidNumber, PoolErrorType.InvalidNumericValue], [ 'amountOut', validNumber, invalidNumber, PoolErrorType.InvalidNumericValue, ], ['feeTier', validFeeTier, invalidFeeTier, PoolErrorType.InvalidFeeTier], ['address', validAddress, invalidAddress, PoolErrorType.InvalidAddress], ['runeAddress', validAddress, invalidAddress, PoolErrorType.InvalidAddress], [ 'paymentAddress', validAddress, invalidAddress, PoolErrorType.InvalidAddress, ], ])( 'Validates field %s: passes with valid value and throws error with invalid value', (field, validValue, invalidValue, expectedErrorType) => { // Test valid case: The request should pass without error. const validRequest = { [field]: validValue }; expect(() => { const result = validatePoolSdkData(validRequest, dummyNetwork); expect(result).toEqual(validRequest); }).not.toThrow(); // Test invalid case: The request should throw a PoolErrorException with the expected type. const invalidRequest = { [field]: invalidValue }; let error: any; try { validatePoolSdkData(invalidRequest, dummyNetwork); } catch (err: any) { error = err; } expect(error.error.type).toEqual(expectedErrorType); }, ); }); describe('validateSaturnConfigData', () => { it('passes valid configuration data', () => { const validConfig = { feeRate: 1n, liquidityAmount: 1n }; expect(() => validateSaturnConfigData(validConfig)).not.toThrow(); }); it('throws an error for invalid configuration data', () => { const invalidConfig = { feeRate: 0n }; let error: any; try { validateSaturnConfigData(invalidConfig); } catch (e) { error = e; } expect(error.error.type).toEqual(PoolErrorType.InvalidFeeRate); }); });