Skip to content

Commit

Permalink
fix: the calculation of the security deposit amount (#436)
Browse files Browse the repository at this point in the history
* fetch currencies from blockchain instead of using harcoded metadata

* handle undefined blockchain data for /gas

* Adjust GetToken and Routing for GET PEN

* implement useAssetRegistryMetadata hook

* update pendulum color palette

* fix get pen button color and /gas get token title

* refactor usePriceFetcher

* add GLMR token icon

* implement PEN styles for Badges

* implement styles for GET PEN button

* fix style of GET PEN submit button

* remove node polyfills for stellar-sdk as stellar-sdk started throwing errors about doubles polyfills definitions

* refactor useBalances to use AssetRegistry instead of Vaults

* fix types

* Update StateUpdater Preact type to newest requirements

* update to typescript 5 and update preact

* update spacewalk animation

* handle 'Native' string input in usePriceFetcher

* change calculation of griefingCollateral in FeeBox (Spacewalk)

* improve GriefingCollateral calculation checks

* extract griefingCollateral calculations to helpers.ts

* implement basic tests for calculateGriefingCollateral

* Refactor price fetcher

* Fix type error

* improve readability of useFeePallet, improve calculateGriefingCollateral(and tests)

* fix display of dashboard decimals

* fix isNativeToken in useBalances

* update error handling for buyout

* fix comparision in usePriceFetcher hook

* improve handleBuyoutError in useBuyout hook

* improve get pen/ampe success dialog

* fix types in usePriceFetcher

* fix types

* Fix calculation of griefing fee

* Refactor calculation into hook

* Limit shown decimals

* Move calculation out of FeeBox

* Remove test

* Fix error for currency code less than 4

* Split `useAccountBalance` into total and transferable

* Fix wrong condition in `useBalances`

* Simplify condition

* Use `_.padEnd()`

* Move `useCalculateGriefingCollateral` to extra file

* implement tests for useCalculateGriefingCollateral hook

* Improve useCalculateGriefingCollateral.test.ts

---------

Co-authored-by: Marcel Ebert <[email protected]>
  • Loading branch information
Sharqiewicz and ebma authored Jun 26, 2024
1 parent 6d7fe1e commit 98bc392
Show file tree
Hide file tree
Showing 15 changed files with 263 additions and 54 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,4 @@ dist-ssr
!.yarn/versions

vite.config.ts.timestamp-*
coverage/
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"lint:ts": "tsc --noEmit",
"test": "jest",
"test:watch": "jest --watchAll=true",
"test:coverage": "jest --coverage",
"codegen": "graphql-codegen --config codegen.ts",
"format": "prettier . --write",
"release": "semantic-release",
Expand Down Expand Up @@ -89,6 +90,7 @@
"@testing-library/jest-dom": "^6.1.6",
"@testing-library/preact": "^3.2.3",
"@testing-library/preact-hooks": "^1.1.0",
"@testing-library/react-hooks": "^8.0.1",
"@testing-library/user-event": "^14.5.2",
"@types/big.js": "^6.2.2",
"@types/jest": "^29.5.11",
Expand Down
4 changes: 2 additions & 2 deletions src/components/GetToken/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,13 @@ const getTokenIcon = (currentTenant: TenantName) => {
};

export const GetToken = () => {
const { balance } = useAccountBalance();
const { total } = useAccountBalance().balances;
const { currentTenant } = useSwitchChain();
const { tokenSymbol } = useNodeInfoState().state;

const link = `/${currentTenant}/gas`;

const isBalanceZero = Number(balance) === 0;
const isBalanceZero = Number(total) === 0;

return (
<section className="flex items-center">
Expand Down
3 changes: 2 additions & 1 deletion src/components/Wallet/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ interface Props {
const OpenWallet = (props: Props): JSX.Element => {
const { walletAccount, dAppName, setWalletAccount, removeWalletAccount } = useGlobalState();
const { wallet, address } = walletAccount || {};
const { query, balance } = useAccountBalance();
const { query, balances } = useAccountBalance();
const { ss58Format, tokenSymbol } = useNodeInfoState().state;
const { total: balance } = balances;

return (
<>
Expand Down
25 changes: 16 additions & 9 deletions src/helpers/spacewalk.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import _ from 'lodash';
import { ApiPromise } from '@polkadot/api';
import { U8aFixed } from '@polkadot/types-codec';
import { H256 } from '@polkadot/types/interfaces';
Expand Down Expand Up @@ -65,27 +66,33 @@ export function currencyToStellarAssetCode(currency: SpacewalkPrimitivesCurrency

export function convertStellarAssetToCurrency(asset: Asset, api: ApiPromise): SpacewalkPrimitivesCurrencyId {
if (asset.isNative()) {
return api.createType('SpacewalkPrimitivesCurrencyId', 'StellarNative');
return api.createType('SpacewalkPrimitivesCurrencyId', { Stellar: 'StellarNative' });
} else {
const pair = Keypair.fromPublicKey(asset.getIssuer());
// We need the raw public key, not the base58 encoded version
const issuerRawPublicKey = pair.rawPublicKey();
const issuer = api.createType('Raw', issuerRawPublicKey, 32);

if (asset.getCode().length <= 4) {
const code = api.createType('Raw', asset.getCode(), 4);
const paddedCode = _.padEnd(asset.getCode(), 4, '\0');
const code = api.createType('Raw', paddedCode, 4);
return api.createType('SpacewalkPrimitivesCurrencyId', {
AlphaNum4: {
code,
issuer,
Stellar: {
AlphaNum4: {
code,
issuer,
},
},
});
} else {
const code = api.createType('Raw', asset.getCode(), 12);
const paddedCode = _.padEnd(asset.getCode(), 12, '\0');
const code = api.createType('Raw', paddedCode, 12);
return api.createType('SpacewalkPrimitivesCurrencyId', {
AlphaNum12: {
code,
issuer,
Stellar: {
AlphaNum12: {
code,
issuer,
},
},
});
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { renderHook } from '@testing-library/react-hooks';
import { waitFor } from '@testing-library/preact';
import { Asset } from 'stellar-base';
import Big from 'big.js';
import { useFeePallet } from '../useFeePallet';
import { useCalculateGriefingCollateral } from '../useCalculateGriefingCollateral';

const STELLAR_ONE_WITH_DECIMALS = 1000000000000;
const VALID_ASSET = new Asset('code', 'GASOCNHNNLYFNMDJYQ3XFMI7BYHIOCFW3GJEOWRPEGK2TDPGTG2E5EDW');
// We assume that the price is the same for all assets
const PRICE = 1;

jest.mock('../../usePriceFetcher', () => ({
usePriceFetcher: jest.fn().mockReturnValue({
getTokenPriceForCurrency: jest.fn().mockReturnValue(Promise.resolve(PRICE)),
}),
}));

jest.mock('../../../NodeInfoProvider', () => ({
useNodeInfoState: jest.fn().mockReturnValue({
state: { api: {} },
}),
}));

jest.mock('../../../helpers/spacewalk', () => ({
convertStellarAssetToCurrency: jest.fn().mockImplementation(() => 'convertedCurrency'),
}));

// useFeePallet mock is implemented in the describe() as we need to spy it during tests
jest.mock('../useFeePallet');

describe('useCalculateGriefingCollateral', () => {
const mockGetFees = jest.fn().mockReturnValue({
griefingCollateralCurrency: 'AUDD',
issueGriefingCollateralFee: 0.05,
});

beforeEach(() => {
(useFeePallet as jest.MockedFunction<typeof useFeePallet>).mockReturnValue({
getFees: mockGetFees,
getTransactionFee: jest.fn(),
});

jest.useFakeTimers();
});

it('should return 0 griefing collateral if amount is not valid', async () => {
const { result } = renderHook(() => useCalculateGriefingCollateral(new Big(0), VALID_ASSET));

const expectation = () => expect(result.current.eq(new Big(0))).toBeTruthy();

expectation();
await waitFor(expectation);
});

it('should return 0 griefing collateral if bridgedAsset is not valid', async () => {
const { result } = renderHook(() => useCalculateGriefingCollateral(new Big(1000), undefined));

const expectation = () => expect(result.current.eq(new Big(0))).toBeTruthy();

expectation();
await waitFor(expectation);
});

it('should return 0 griefing collateral if griefingCollateralCurrency is not valid', async () => {
mockGetFees.mockReturnValueOnce({
griefingCollateralCurrency: undefined,
issueGriefingCollateralFee: 0.05,
});

const { result } = renderHook(() => useCalculateGriefingCollateral(new Big(1000), VALID_ASSET));

const expectation = () => expect(result.current.eq(new Big(0))).toBeTruthy();

expectation();
await waitFor(expectation);
});

it('should calculate griefing collateral correctly', async () => {
const amount = new Big(STELLAR_ONE_WITH_DECIMALS);
const { result } = renderHook(() => useCalculateGriefingCollateral(amount, VALID_ASSET));

// First returned value (before useEffect is fired)
expect(result.current.eq(0)).toBeTruthy();

const griefingFee = mockGetFees().issueGriefingCollateralFee;
// Since we assume the same price for all assets, the decimal griefing collateral is the amount * griefingFee / decimals
const griefingAmount = amount
.times(griefingFee)
.div(10 ** 12)
.toNumber();

await waitFor(() => expect(result.current.eq(griefingAmount)).toBeTruthy());
});
});
49 changes: 49 additions & 0 deletions src/hooks/spacewalk/useCalculateGriefingCollateral.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Asset } from 'stellar-base';
import Big from 'big.js';
import { useEffect, useState, useMemo } from 'preact/compat';
import { nativeStellarToDecimal } from '../../shared/parseNumbers/metric';
import { convertStellarAssetToCurrency } from '../../helpers/spacewalk';
import { useNodeInfoState } from '../../NodeInfoProvider';
import { usePriceFetcher } from '../usePriceFetcher';
import { useFeePallet } from './useFeePallet';

const isInvalid = (value: unknown) => {
return !value;
};

export const useCalculateGriefingCollateral = (amount: Big, bridgedAsset?: Asset): Big.Big => {
const { getTokenPriceForCurrency } = usePriceFetcher();
const { getFees } = useFeePallet();
const [griefingCollateral, setGriefingCollateral] = useState<Big>(new Big(0));
const { api } = useNodeInfoState().state;

const { griefingCollateralCurrency, issueGriefingCollateralFee } = getFees();

const bridgedCurrency = useMemo(() => {
return bridgedAsset && api ? convertStellarAssetToCurrency(bridgedAsset, api) : null;
}, [bridgedAsset, api]);

useEffect(() => {
const calculateGriefingCollateral = async () => {
if (isInvalid(amount) || isInvalid(bridgedCurrency) || isInvalid(griefingCollateralCurrency)) return;

if (bridgedCurrency && griefingCollateralCurrency) {
try {
const assetUSDPrice = await getTokenPriceForCurrency(bridgedCurrency);
const amountUSD = nativeStellarToDecimal(amount).mul(assetUSDPrice);

const griefingCollateralCurrencyUSD = await getTokenPriceForCurrency(griefingCollateralCurrency);
if (isInvalid(griefingCollateralCurrencyUSD)) return;

setGriefingCollateral(amountUSD.mul(issueGriefingCollateralFee).div(griefingCollateralCurrencyUSD));
} catch (error) {
console.error('Error calculating griefing collateral:', error);
}
}
};

calculateGriefingCollateral();
}, [amount, getTokenPriceForCurrency, griefingCollateralCurrency, issueGriefingCollateralFee, bridgedCurrency]);

return griefingCollateral;
};
22 changes: 11 additions & 11 deletions src/hooks/spacewalk/useFeePallet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ export function useFeePallet() {
const [redeemFee, setRedeemFee] = useState<Big>(new Big(0));
const [punishmentFee, setPunishmentFee] = useState<Big>(new Big(0));
const [premiumRedeemFee, setPremiumRedeemFee] = useState<Big>(new Big(0));
const [issueGriefingCollateral, setIssueGriefingCollateral] = useState<Big>(new Big(0));
const [replaceGriefingCollateral, setReplaceGriefingCollateral] = useState<Big>(new Big(0));
const [issueGriefingCollateralFee, setIssueGriefingCollateralFee] = useState<Big>(new Big(0));
const [replaceGriefingCollateralFee, setReplaceGriefingCollateralFee] = useState<Big>(new Big(0));

const [griefingCollateralCurrency, setGriefingCollateralCurrency] = useState<
SpacewalkPrimitivesCurrencyId | undefined
Expand Down Expand Up @@ -50,17 +50,17 @@ export function useFeePallet() {
const decimal = Big(fixedPointToDecimal(fee.toString()));
setPremiumRedeemFee(decimal);
}),
api.query.fee.issueGriefingCollateral((fee) => {
api.query.fee.issueGriefingCollateral((fee: Big) => {
const decimal = Big(fixedPointToDecimal(fee.toString()));
setIssueGriefingCollateral(decimal);
setIssueGriefingCollateralFee(decimal);
}),
api.query.fee.replaceGriefingCollateral((fee) => {
api.query.fee.replaceGriefingCollateral((fee: Big) => {
const decimal = Big(fixedPointToDecimal(fee.toString()));
setReplaceGriefingCollateral(decimal);
setReplaceGriefingCollateralFee(decimal);
}),
]).then((unsubscribeFunctions) => {
unsubscribe = () => {
unsubscribeFunctions.forEach((u) => u());
unsubscribeFunctions.forEach((u) => (u as () => void)());
};
});

Expand All @@ -75,8 +75,8 @@ export function useFeePallet() {
redeemFee,
punishmentFee,
premiumRedeemFee,
issueGriefingCollateral,
replaceGriefingCollateral,
issueGriefingCollateralFee,
replaceGriefingCollateralFee,
griefingCollateralCurrency,
};
},
Expand All @@ -99,8 +99,8 @@ export function useFeePallet() {
redeemFee,
punishmentFee,
premiumRedeemFee,
issueGriefingCollateral,
replaceGriefingCollateral,
issueGriefingCollateralFee,
replaceGriefingCollateralFee,
griefingCollateralCurrency,
]);

Expand Down
8 changes: 5 additions & 3 deletions src/hooks/useAssetRegistryMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,11 @@ interface UseAssetRegistryMetadata {

function convertToOrmlAssetRegistryAssetMetadata(metadata: [StorageKey, Codec]): OrmlTraitsAssetRegistryAssetMetadata {
const { decimals, name, symbol, additional } = metadata[1].toHuman() as CurrencyMetadataType;
const currencyIdArray = metadata[0].toHuman() as unknown as SpacewalkPrimitivesCurrencyId[];
const currencyId = currencyIdArray[0];

const currencyIdStorageKey = metadata[0] as StorageKey;
// We need to convert the currencyId StorageKey to the correct type
const currencyIdJsonArray = currencyIdStorageKey.toHuman() as unknown as SpacewalkPrimitivesCurrencyId[];
const currencyIdJson = currencyIdJsonArray[0];
const currencyId = currencyIdStorageKey.registry.createType('SpacewalkPrimitivesCurrencyId', currencyIdJson);
return {
metadata: { decimals: Number(decimals), name, symbol, additional },
currencyId,
Expand Down
4 changes: 2 additions & 2 deletions src/hooks/useBalances.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ function useBalances() {
const fetchTokenBalance = useCallback(
async (address: string, currencyId: SpacewalkPrimitivesCurrencyId) => {
if (!api) return;
const isNativeToken = typeof currencyId === 'string' && currencyId === 'Native';
const isNativeToken = currencyId.toHuman() === 'Native';
if (isNativeToken) {
return api.query.system.account(address);
}
Expand All @@ -42,7 +42,7 @@ function useBalances() {
const walletAddress = ss58Format ? getAddressForFormat(walletAccount.address, ss58Format) : walletAccount.address;

const getFree = (tokenBalanceRaw: unknown, asset: OrmlTraitsAssetRegistryAssetMetadata) => {
const isNativeToken = typeof asset.currencyId === 'string' && asset.currencyId === 'Native';
const isNativeToken = asset.currencyId.toHuman() === 'Native';
if (isNativeToken) {
return (tokenBalanceRaw as { data: { free: Big } }).data.free;
}
Expand Down
15 changes: 13 additions & 2 deletions src/hooks/usePriceFetcher.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { useCallback, useEffect, useState } from 'preact/compat';
import { TenantName } from '../models/Tenant';
import useSwitchChain from './useSwitchChain';
import { useNodeInfoState } from '../NodeInfoProvider';
import { nativeToDecimal } from '../shared/parseNumbers/metric';
import { SpacewalkPrimitivesCurrencyId } from '@polkadot/types/lookup';
Expand All @@ -19,6 +21,7 @@ type PricesCache = { [diaKeys: string]: number };

export const usePriceFetcher = () => {
const [pricesCache, setPricesCache] = useState<PricesCache>({});
const { currentTenant } = useSwitchChain();
const { api } = useNodeInfoState().state;
const { getAllAssetsMetadata } = useAssetRegistryMetadata();

Expand Down Expand Up @@ -64,15 +67,23 @@ export const usePriceFetcher = () => {
[pricesCache],
);

const handleNativeTokenPrice = useCallback(() => {
if (currentTenant === TenantName.Pendulum) return pricesCache['Pendulum:PEN'];
return pricesCache['Amplitude:AMPE'];
}, [currentTenant, pricesCache]);

const getTokenPriceForCurrency = useCallback(
async (currency: SpacewalkPrimitivesCurrencyId) => {
const asset = getAllAssetsMetadata().find((asset) => isEqual(asset.currencyId, currency));
if (currency.toHuman() === 'Native') return handleNativeTokenPrice();
const asset = getAllAssetsMetadata().find((asset) => {
return isEqual(asset.currencyId.toHuman(), currency.toHuman());
});
if (!asset) {
return 0;
}
return getTokenPriceForKeys(asset.metadata.additional.diaKeys);
},
[getAllAssetsMetadata, getTokenPriceForKeys],
[getAllAssetsMetadata, getTokenPriceForKeys, handleNativeTokenPrice],
);

return { getTokenPriceForKeys, getTokenPriceForCurrency };
Expand Down
Loading

0 comments on commit 98bc392

Please sign in to comment.