Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Downgrade to stable Starknet version #9

Merged
merged 3 commits into from
May 17, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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 package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"dependencies": {
"@starkware-industries/starkware-crypto-utils": "^0.2.1",
"bignumber.js": "^9.1.2",
"starknet": "^6.6.6"
"starknet": "^5.24.3"
},
"devDependencies": {
"@ianvs/prettier-plugin-sort-imports": "^4.2.1",
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,6 @@ export const ParaclearProvider = {

export const Paraclear = {
getTokenBalance: _Paraclear.getTokenBalance,
getSocializedLossFactor: _Paraclear.getSocializedLossFactor,
getReceivableAmount: _Paraclear.getReceivableAmount,
};
131 changes: 126 additions & 5 deletions src/paraclear.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export async function getTokenBalance(
throw new Error(`Token ${params.token} is not supported`);
}

const [result] = await params.provider.callContract(
const { result } = await params.provider.callContract(
{
contractAddress: params.config.paraclearAddress,
entrypoint: 'getTokenAssetBalance',
Expand All @@ -52,22 +52,143 @@ export async function getTokenBalance(
'latest',
);

if (result == null) {
const value = result?.[0];

if (value == null) {
throw new Error('Failed to get token balance');
}

const resultBn = new BigNumber(result);
if (resultBn.isNaN()) {
const valueBn = new BigNumber(value);
if (valueBn.isNaN()) {
throw new Error('Failed to parse token balance');
}

const chainSizeBn = resultBn;
const chainSizeBn = valueBn;

const sizeBn = fromChainSize(chainSizeBn, params.config.paraclearDecimals);

return { size: sizeBn.toString() };
}

interface GetSocializedLossFactorParams {
readonly config: ParadexConfig;
readonly provider: Pick<ParaclearProvider, 'callContract'>;
}

interface GetSocializedLossFactorResult {
readonly socializedLossFactor: string;
}

/**
* Socialized losses happen when Paradex Insurance Fund is bankrupt
* due to large amounts of unprofitable liquidations. When socialized
* losses are active, (socialized loss factor > 0), the amount that
* the user will receive when withdrawing will be smaller than the
* requested amount.
*/
export async function getSocializedLossFactor(
params: GetSocializedLossFactorParams,
): Promise<GetSocializedLossFactorResult> {
const { result } = await params.provider.callContract({
contractAddress: params.config.paraclearAddress,
entrypoint: 'getSocializedLossFactor',
});

const value = result?.[0];

if (value == null) {
throw new Error('Failed to get socialized loss factor');
}

const valueBn = new BigNumber(value);
if (valueBn.isNaN()) {
throw new Error('Failed to parse socialized loss factor');
}

const chainFactorBn = valueBn;

const factorBn = fromChainSize(
chainFactorBn,
params.config.paraclearDecimals,
);

return { socializedLossFactor: factorBn.toString() };
}

interface GetReceivableAmountParams {
readonly config: ParadexConfig;
readonly provider: Pick<ParaclearProvider, 'callContract'>;
/**
* Amount of to withdraw from Paradex, as a decimal string.
* The receivable amount will be calculated based on this amount and
* can be less than the requested amount if socialized loss is active.
*/
readonly amount: string;
}

interface GetReceivableAmountResult {
/**
* Amount that will be received from Paradex, after socialized loss,
* if applicable, after a withdrawal of the given amount parameter.
* Decimal string.
* @example '99.45'
*/
readonly receivableAmount: string;
/**
* The receivable amount, converted to be used in chain calls,
* using the Paraclear decimals.
* @example '9945000000'
*/
readonly receivableAmountChain: string;
/**
* Socialized loss factor used to calculate the receivable amount.
* Decimal string.
* @example '0.05'
*/
readonly socializedLossFactor: string;
}

/**
* The receivable amount is calculated based on the current
* socialized loss factor: amount * (1 - socializedLossFactor)
*
* If the socialized loss factor is 0, the receivable amount
* will be equal to the requested amount.
*/
export async function getReceivableAmount(
params: GetReceivableAmountParams,
): Promise<GetReceivableAmountResult> {
const amountBn = new BigNumber(params.amount);

if (amountBn.isNaN()) {
throw new Error('Invalid amount');
}

const { socializedLossFactor } = await getSocializedLossFactor({
config: params.config,
provider: params.provider,
});

const receivableAmount = amountBn.times(
BigNumber(1).minus(socializedLossFactor),
);

const receivableAmountChain = toChainSize(
receivableAmount.toString(),
params.config.paraclearDecimals,
);

return {
receivableAmount: receivableAmount.toString(),
receivableAmountChain: receivableAmountChain.toString(),
socializedLossFactor,
};
}

function fromChainSize(size: BigNumber, decimals: number): string {
return new BigNumber(size).div(10 ** decimals).toString();
}

function toChainSize(size: string, decimals: number): BigNumber {
return new BigNumber(size).times(10 ** decimals);
}
126 changes: 121 additions & 5 deletions tests/paraclear.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { getTokenBalance } from '../src/paraclear';
import {
getReceivableAmount,
getSocializedLossFactor,
getTokenBalance,
} from '../src/paraclear';

import { configFactory } from './factories/paradex-config';

describe('getBalance', () => {
test('should return the balance size', async () => {
const mockProvider = { callContract: jest.fn() };
mockProvider.callContract.mockResolvedValueOnce([9900000000]);
mockProvider.callContract.mockResolvedValueOnce({ result: [9900000000] });

const result = await getTokenBalance({
config: configFactory(),
Expand All @@ -19,7 +23,7 @@ describe('getBalance', () => {

test('should throw an error if token is not supported', async () => {
const mockProvider = { callContract: jest.fn() };
mockProvider.callContract.mockResolvedValueOnce([9900000000]);
mockProvider.callContract.mockResolvedValueOnce({ result: [9900000000] });

const result = getTokenBalance({
config: configFactory(),
Expand All @@ -33,7 +37,7 @@ describe('getBalance', () => {

test('should throw an error if calling the contract returns no value', async () => {
const mockProvider = { callContract: jest.fn() };
mockProvider.callContract.mockResolvedValueOnce([]);
mockProvider.callContract.mockResolvedValueOnce({ result: [] });

const result = getTokenBalance({
config: configFactory(),
Expand All @@ -47,7 +51,9 @@ describe('getBalance', () => {

test('should throw an error if balance parsing fails', async () => {
const mockProvider = { callContract: jest.fn() };
mockProvider.callContract.mockResolvedValueOnce(['not a number']);
mockProvider.callContract.mockResolvedValueOnce({
result: ['not a number'],
});

const result = getTokenBalance({
config: configFactory(),
Expand All @@ -59,3 +65,113 @@ describe('getBalance', () => {
await expect(result).rejects.toThrow('Failed to parse token balance');
});
});

describe('getSocializedLossFactor', () => {
test('should return the socialized loss factor', async () => {
const mockProvider = { callContract: jest.fn() };
mockProvider.callContract.mockResolvedValueOnce({ result: ['123456789'] });

const result = await getSocializedLossFactor({
config: configFactory(),
provider: mockProvider,
});

expect(result).toEqual({ socializedLossFactor: '1.23456789' });
});

test('should throw an error if the result is null', async () => {
const mockProvider = { callContract: jest.fn() };
mockProvider.callContract.mockResolvedValueOnce({ result: [null] });

const result = getSocializedLossFactor({
config: configFactory(),
provider: mockProvider,
});

await expect(result).rejects.toThrow(
'Failed to get socialized loss factor',
);
});

test('should throw an error if the result is not a number', async () => {
const mockProvider = { callContract: jest.fn() };
mockProvider.callContract.mockResolvedValueOnce({
result: ['not a number'],
});

const result = getSocializedLossFactor({
config: configFactory(),
provider: mockProvider,
});

await expect(result).rejects.toThrow(
'Failed to parse socialized loss factor',
);
});
});

describe('getReceivableAmount', () => {
test('should calculate the receivable amount correctly', async () => {
const socializedLossFactor = '0.01';
const socializedLossFactorChain = '1000000';

const mockProvider = { callContract: jest.fn() };
mockProvider.callContract.mockResolvedValueOnce({
result: [socializedLossFactorChain],
});

const result = await getReceivableAmount({
config: configFactory(),
provider: mockProvider,
amount: '100',
});

expect(result).toStrictEqual({
receivableAmount: '99',
receivableAmountChain: '9900000000',
socializedLossFactor,
});
});

test('should throw an error for invalid amount', async () => {
const result = getReceivableAmount({
config: configFactory(),
provider: { callContract: jest.fn() },
amount: 'not a number',
});

await expect(result).rejects.toThrow('Invalid amount');
});

test('should throw an error if socialized loss factor is not a number', async () => {
const mockProvider = { callContract: jest.fn() };
mockProvider.callContract.mockResolvedValueOnce({
result: ['not a number'],
});

const result = getReceivableAmount({
config: configFactory(),
provider: mockProvider,
amount: '100',
});

await expect(result).rejects.toThrow(
'Failed to parse socialized loss factor',
);
});

test('should throw an error if socialized loss factor is null', async () => {
const mockProvider = { callContract: jest.fn() };
mockProvider.callContract.mockResolvedValueOnce({ result: [null] });

const result = getReceivableAmount({
config: configFactory(),
provider: mockProvider,
amount: '100',
});

await expect(result).rejects.toThrow(
'Failed to get socialized loss factor',
);
});
});
Loading