Skip to content
Merged
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
2 changes: 1 addition & 1 deletion ui/ts/components/LiquidationModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ export function LiquidationModal({
</div>
</div>
<div className='workflow-metric-grid'>
<MetricField label='REP Deposit'>
<MetricField label='REP Collateral'>
<CurrencyValue value={liquidationSimulation.callerAfter.repDepositShare} suffix='REP' />
</MetricField>
<MetricField label='Security Bond Allowance'>
Expand Down
16 changes: 10 additions & 6 deletions ui/ts/components/SecurityPoolWorkflowSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ export function SecurityPoolWorkflowSection({
const hasValidOraclePrice = hasValidSecurityVaultOraclePrice(selectedVaultDetails?.managerAddress, currentPoolOracleManagerDetails)
const depositAmount = (() => {
try {
return parseRepAmountInput(securityVault.securityVaultForm.depositAmount ?? '', 'REP deposit amount')
return parseRepAmountInput(securityVault.securityVaultForm.depositAmount ?? '', 'REP collateral amount')
} catch {
return undefined
}
Expand Down Expand Up @@ -347,7 +347,7 @@ export function SecurityPoolWorkflowSection({
const vaultReadinessActions = getSecurityPoolVaultReadinessActions([
{
actionLabel: 'Deposit REP',
description: 'Add REP to the selected vault, including approval from inside the operation.',
description: 'Add REP to the selected vault.',
key: 'deposit-rep',
onAction: () => setVaultActionModal('deposit-rep'),
readiness: depositGuardMessage === undefined ? 'ready' : 'warning',
Expand Down Expand Up @@ -569,7 +569,7 @@ export function SecurityPoolWorkflowSection({
<MetricField label='Open Interest Fee / Year'>
<CurrencyValue value={openInterestFeePerYearBigint(loadedSelectedPool.currentRetentionRate)} suffix='%' />
</MetricField>
<MetricField label='Total REP Deposited'>
<MetricField label='Total REP Collateral'>
<CurrencyValue value={loadedSelectedPool.totalRepDeposit} suffix='REP' />
</MetricField>
<MetricField label='Total Security Bond Allowance'>
Expand Down Expand Up @@ -888,7 +888,7 @@ export function SecurityPoolWorkflowSection({
</div>
</section>

<OperationModal isOpen={vaultActionModal === 'deposit-rep'} onClose={() => setVaultActionModal(undefined)} title='Deposit REP' description='Review the selected vault, complete REP approval if needed, then deposit REP.'>
<OperationModal isOpen={vaultActionModal === 'deposit-rep'} onClose={() => setVaultActionModal(undefined)} title='Deposit REP' description='Review the selected vault, then deposit REP.'>
{selectedVaultDetails === undefined ? <p className='detail'>Refresh the selected vault before depositing REP.</p> : null}
{selectedVaultDetails === undefined ? null : (
<>
Expand All @@ -904,7 +904,7 @@ export function SecurityPoolWorkflowSection({
variant='embedded'
/>
<label className='field'>
<span>REP Deposit Amount</span>
<span>REP Collateral Amount</span>
<div className='field-inline'>
<FormInput className='field-inline-input' value={securityVault.securityVaultForm.depositAmount} onInput={event => securityVault.onSecurityVaultFormChange({ depositAmount: event.currentTarget.value })} />
<button
Expand All @@ -920,6 +920,11 @@ export function SecurityPoolWorkflowSection({
</button>
</div>
</label>
<div className='workflow-metric-grid'>
<MetricField label='Wallet REP'>
<CurrencyValue value={securityVault.securityVaultRepBalance} suffix='REP' />
</MetricField>
</div>
<TokenApprovalControl
actionLabel='depositing REP'
allowanceError={securityVault.securityVaultRepApproval.error}
Expand All @@ -937,7 +942,6 @@ export function SecurityPoolWorkflowSection({
<RequirementsChecklist
items={[
{ key: 'owned', label: 'Selected vault is owned by the connected account', resolved: selectedVaultIsOwnedByAccount },
{ key: 'approval', label: 'REP approval is sufficient for the deposit amount', resolved: approvalRequirement.hasSufficientApproval, ...(approvalRequirement.hasSufficientApproval ? {} : { detail: 'Approve REP inside this modal before depositing.' }) },
{ key: 'balance', label: 'Wallet REP balance covers the deposit amount', resolved: repBalanceGap === undefined || repBalanceGap <= 0n, ...(repBalanceGap !== undefined && repBalanceGap > 0n ? { detail: `Need ${formatCurrencyBalance(repBalanceGap)} more REP.` } : {}) },
{ key: 'minimum', label: 'First deposit meets the vault minimum', resolved: !isDepositBelowMinimum, ...(isDepositBelowMinimum ? { detail: `First deposits must be at least ${formatCurrencyBalance(MIN_SECURITY_VAULT_REP_DEPOSIT)} REP.` } : {}) },
]}
Expand Down
4 changes: 2 additions & 2 deletions ui/ts/components/SecurityVaultSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export function SecurityVaultSection({
const selectedVaultIsOwnedByAccount = isSelectedVaultOwnedByAccountHelper(selectedVaultAddress, accountState.address)
const depositAmount = (() => {
try {
return parseRepAmountInput(normalizedSecurityVaultForm.depositAmount, 'REP deposit amount')
return parseRepAmountInput(normalizedSecurityVaultForm.depositAmount, 'REP collateral amount')
} catch {
return undefined
}
Expand Down Expand Up @@ -287,7 +287,7 @@ export function SecurityVaultSection({

<SectionBlock title='Deposit REP'>
<label className='field'>
<span>REP Deposit Amount</span>
<span>REP Collateral Amount</span>
<div className='field-inline'>
<FormInput className='field-inline-input' value={normalizedSecurityVaultForm.depositAmount} onInput={event => onSecurityVaultFormChange({ depositAmount: event.currentTarget.value })} />
<button
Expand Down
2 changes: 1 addition & 1 deletion ui/ts/components/VaultMetricGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export function VaultMetricGrid({ approvedRep, className = '', lockedRepInEscala

return (
<div className={[gridClassName, className].filter(Boolean).join(' ')}>
<MetricField className={metricClassName} label='Rep Deposit'>
<MetricField className={metricClassName} label='REP Collateral'>
<CurrencyValue value={repDepositShare} suffix='REP' />
</MetricField>
{approvedRep === undefined ? undefined : (
Expand Down
4 changes: 2 additions & 2 deletions ui/ts/hooks/useSecurityVaultOperations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ export function useSecurityVaultOperations({ accountAddress, enabled, onTransact
async (vaultAddress, securityPoolAddress) => {
const details = await loadExistingSecurityVaultDetails(securityPoolAddress, vaultAddress, 'Security pool does not exist')
if (details === undefined) return undefined
const approvalAmount = amount ?? parseRepAmountInput(securityVaultForm.value.depositAmount, 'REP deposit amount')
const approvalAmount = amount ?? parseRepAmountInput(securityVaultForm.value.depositAmount, 'REP collateral amount')
return await approveErc20(createWalletWriteClient(vaultAddress, { onTransactionSubmitted }), details.repToken, securityPoolAddress, approvalAmount, 'approveRep')
},
'Failed to approve REP',
Expand All @@ -199,7 +199,7 @@ export function useSecurityVaultOperations({ accountAddress, enabled, onTransact
await runVaultAction(
'depositRep',
async (vaultAddress, securityPoolAddress) => {
const depositAmount = parseRepAmountInput(securityVaultForm.value.depositAmount, 'REP deposit amount')
const depositAmount = parseRepAmountInput(securityVaultForm.value.depositAmount, 'REP collateral amount')
const details = await loadExistingSecurityVaultDetails(securityPoolAddress, vaultAddress, 'Security pool does not exist')
if (details === undefined) return undefined
const currentRepBalance = await loadErc20Balance(createConnectedReadClient(), details.repToken, vaultAddress)
Expand Down
4 changes: 2 additions & 2 deletions ui/ts/lib/liquidation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,9 @@ export function getLiquidationFailureReason({
})
if (simulation.debtToMove <= 0n) return 'This vault has no debt available to move.'
if (isVaultLiquidatable(repPerEthPrice, simulation.callerAfter.securityBondAllowance, simulation.callerAfter.repDepositShare, securityMultiplier)) return 'The caller vault would become liquidatable after this liquidation.'
if (simulation.targetAfter.repDepositShare !== 0n && simulation.targetAfter.repDepositShare < MIN_REP_DEPOSIT) return 'The target vault would fall below the minimum REP deposit after liquidation.'
if (simulation.targetAfter.repDepositShare !== 0n && simulation.targetAfter.repDepositShare < MIN_REP_DEPOSIT) return 'The target vault would fall below the minimum REP collateral after liquidation.'
if (simulation.targetAfter.securityBondAllowance !== 0n && simulation.targetAfter.securityBondAllowance < MIN_SECURITY_BOND_DEBT) return 'The target vault would fall below the minimum security bond allowance after liquidation.'
if (simulation.callerAfter.repDepositShare < MIN_REP_DEPOSIT) return 'The caller vault would remain below the minimum REP deposit after liquidation.'
if (simulation.callerAfter.repDepositShare < MIN_REP_DEPOSIT) return 'The caller vault would remain below the minimum REP collateral after liquidation.'
if (simulation.callerAfter.securityBondAllowance < MIN_SECURITY_BOND_DEBT) return 'The caller vault would remain below the minimum security bond allowance after liquidation.'
return undefined
}
50 changes: 48 additions & 2 deletions ui/ts/tests/securityPoolWorkflowSection.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -390,15 +390,15 @@ describe('SecurityPoolWorkflowSection', () => {
expect(documentQueries.queryByRole('heading', { name: 'Selected Pool Summary' })).toBeNull()
expect(documentQueries.queryByText('Workflow')).toBeNull()
expect(documentQueries.getByText('Question description')).not.toBeNull()
expect(documentQueries.getByText('Total REP Deposited')).not.toBeNull()
expect(documentQueries.getByText('Total REP Collateral')).not.toBeNull()
expect(documentQueries.getByText('Open Oracle Price')).not.toBeNull()
expect(documentQueries.queryByText('Oracle Expires In')).toBeNull()
const selectedPoolContext = document.body.querySelector('.sticky-object-context.static')
if (!(selectedPoolContext instanceof HTMLElement)) {
throw new Error('Expected a non-sticky selected pool context card')
}
const lookupLabel = within(selectedPoolContext).getByText('Security Pool Address')
const firstSummaryMetric = within(selectedPoolContext).getByText('Total REP Deposited')
const firstSummaryMetric = within(selectedPoolContext).getByText('Total REP Collateral')
const lookupPosition = selectedPoolContext.textContent?.indexOf(lookupLabel.textContent ?? '') ?? -1
const summaryPosition = selectedPoolContext.textContent?.indexOf(firstSummaryMetric.textContent ?? '') ?? -1
expect(lookupPosition).toBeGreaterThanOrEqual(0)
Expand Down Expand Up @@ -853,6 +853,52 @@ describe('SecurityPoolWorkflowSection', () => {
expectTransactionButtonDisabled(document.body, 'Claim Fees', 'Refresh the selected vault first.')
})

test('keeps REP approval guidance inside the approval control in the deposit modal', async () => {
const selectedPoolAddress = zeroAddress
const renderedComponent = await renderIntoDocument(
<SecurityPoolWorkflowSection
{...createSecurityPoolWorkflowProps({
accountState: createAccountState(),
securityPoolAddress: selectedPoolAddress,
securityPools: [createSelectedPool({ securityPoolAddress: selectedPoolAddress })],
securityVault: createSecurityVaultProps({
securityVaultDetails: createSecurityVaultDetails({ securityPoolAddress: selectedPoolAddress }),
securityVaultForm: {
depositAmount: '10',
repWithdrawAmount: '',
securityBondAllowanceAmount: '',
securityPoolAddress: selectedPoolAddress,
selectedVaultAddress: zeroAddress,
},
securityVaultRepBalance: 25n * 10n ** 18n,
securityVaultRepApproval: {
error: undefined,
loading: false,
value: 0n,
},
}),
selectedPoolView: 'vaults',
})}
showHeader={false}
/>,
)
cleanupRenderedComponent = renderedComponent.cleanup

const documentQueries = within(document.body)
await act(() => {
fireEvent.click(documentQueries.getAllByRole('button', { name: 'Deposit REP' })[0] as HTMLElement)
})

const depositDialog = documentQueries.getByRole('dialog', { name: 'Deposit REP' })
const modalQueries = within(depositDialog)
expect(modalQueries.queryByText('Review the selected vault, complete REP approval if needed, then deposit REP.')).toBeNull()
expect(modalQueries.queryByText('REP approval is sufficient for the deposit amount')).toBeNull()
expect(modalQueries.queryByText('Approve REP inside this modal before depositing.')).toBeNull()
expect(modalQueries.getByText('Wallet REP')).not.toBeNull()
expect(modalQueries.getByText('Required REP')).not.toBeNull()
expect(modalQueries.getByText('REP Approval Amount')).not.toBeNull()
})

test('caps REP withdrawals to the oracle-backed amount in the seeded security-pool shape', async () => {
const selectedPoolAddress = zeroAddress
const renderedComponent = await renderIntoDocument(
Expand Down
2 changes: 1 addition & 1 deletion ui/ts/tests/securityPoolsSection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -537,7 +537,7 @@ void describe('SecurityPoolsSection', () => {
expect(contextQueries.queryByRole('tab', { name: 'Operate' })).toBeNull()
expect(documentQueries.queryByRole('heading', { name: 'Security pools' })).toBeNull()
const lookupLabel = contextQueries.getByText('Security Pool Address')
const summaryMetric = contextQueries.getByText('Total REP Deposited')
const summaryMetric = contextQueries.getByText('Total REP Collateral')
const lookupPosition = selectedPoolContext.textContent?.indexOf(lookupLabel.textContent ?? '') ?? -1
const summaryPosition = selectedPoolContext.textContent?.indexOf(summaryMetric.textContent ?? '') ?? -1
expect(lookupPosition).toBeGreaterThanOrEqual(0)
Expand Down
4 changes: 2 additions & 2 deletions ui/ts/tests/securityVault.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ void describe('security vault helpers', () => {
})

void test('parses security vault REP inputs as 18-decimal token amounts', () => {
expect(parseRepAmountInput('10', 'REP deposit amount')).toBe(MIN_SECURITY_VAULT_REP_DEPOSIT)
expect(parseRepAmountInput('10.5', 'REP deposit amount')).toBe(105n * 10n ** 17n)
expect(parseRepAmountInput('10', 'REP collateral amount')).toBe(MIN_SECURITY_VAULT_REP_DEPOSIT)
expect(parseRepAmountInput('10.5', 'REP collateral amount')).toBe(105n * 10n ** 17n)
expect(parseRepAmountInput('0.25', 'REP withdraw amount')).toBe(25n * 10n ** 16n)
})

Expand Down
1 change: 1 addition & 0 deletions ui/ts/tests/securityVaultSection.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ describe('SecurityVaultSection', () => {

const documentQueries = within(document.body)
expect(documentQueries.getByText('Selected Vault')).not.toBeNull()
expect(documentQueries.getAllByText('REP Collateral').length).toBeGreaterThan(0)
expect(documentQueries.getAllByText('Approved REP').length).toBeGreaterThan(0)
expect(documentQueries.getAllByText('Locked REP').length).toBeGreaterThan(0)
})
Expand Down
Loading