-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add basic implementation to stake/unstake
- Loading branch information
Showing
5 changed files
with
328 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
import { hemi, hemiSepolia } from 'hemi-viem' | ||
import { HemiPublicClient, HemiWalletClient } from 'hooks/useHemiClient' | ||
import { EvmToken } from 'types/token' | ||
import { canSubmit, stake } from 'utils/stake' | ||
import { getErc20TokenBalance } from 'utils/token' | ||
import { parseUnits, zeroAddress } from 'viem' | ||
import { beforeEach, describe, expect, it, vi } from 'vitest' | ||
|
||
vi.mock('utils/token', () => ({ | ||
getErc20TokenBalance: vi.fn(), | ||
})) | ||
|
||
// @ts-expect-error Adding minimal properties needed | ||
const token: EvmToken = { | ||
chainId: hemiSepolia.id, | ||
decimals: 18, | ||
symbol: 'fakeToken', | ||
} | ||
|
||
describe('utils/stake', function () { | ||
describe('canSubmit', function () { | ||
it('should return error if amount is a negative value', function () { | ||
const result = canSubmit({ | ||
amount: BigInt(-123), | ||
balance: BigInt(1000), | ||
connectedChainId: hemiSepolia.id, | ||
token, | ||
}) | ||
expect(result).toEqual({ error: 'amount-less-equal-than-0' }) | ||
}) | ||
|
||
it('should return error if amount is equal to 0', function () { | ||
const result = canSubmit({ | ||
amount: BigInt(0), | ||
balance: BigInt(1000), | ||
connectedChainId: hemiSepolia.id, | ||
token, | ||
}) | ||
expect(result).toEqual({ error: 'amount-less-equal-than-0' }) | ||
}) | ||
|
||
it('should return error if chain ID does not match', function () { | ||
const result = canSubmit({ | ||
amount: BigInt(1), | ||
balance: BigInt(1000), | ||
connectedChainId: hemi.id, | ||
token, | ||
}) | ||
expect(result).toEqual({ error: 'wrong-chain' }) | ||
}) | ||
|
||
it('should return error if balance is less than or equal to 0', function () { | ||
const result = canSubmit({ | ||
amount: BigInt(1), | ||
balance: BigInt(0), | ||
connectedChainId: hemiSepolia.id, | ||
token, | ||
}) | ||
expect(result).toEqual({ error: 'not-enough-balance' }) | ||
}) | ||
|
||
it('should return error if balance is less than the amount', function () { | ||
const result = canSubmit({ | ||
amount: BigInt(10), | ||
balance: BigInt(9), | ||
connectedChainId: hemiSepolia.id, | ||
token, | ||
}) | ||
expect(result).toEqual({ error: 'amount-larger-than-balance' }) | ||
}) | ||
|
||
it('should return empty object if all conditions are met', function () { | ||
const result = canSubmit({ | ||
amount: BigInt(1), | ||
balance: BigInt(1000), | ||
connectedChainId: hemiSepolia.id, | ||
token, | ||
}) | ||
expect(result).toEqual({}) | ||
}) | ||
}) | ||
|
||
describe('stake', function () { | ||
beforeEach(function () { | ||
vi.clearAllMocks() | ||
}) | ||
|
||
// @ts-expect-error only add the minimum values required | ||
const hemiPublicClient: HemiPublicClient = { chain: hemiSepolia } | ||
// @ts-expect-error only add the minimum values required | ||
const hemiWalletClient: HemiWalletClient = { | ||
stakeToken: vi.fn(), | ||
} | ||
|
||
it('should throw error if the consumer can not stake', async function () { | ||
getErc20TokenBalance.mockResolvedValue(BigInt(0)) | ||
await expect( | ||
stake({ | ||
amount: '1', | ||
from: zeroAddress, | ||
hemiPublicClient, | ||
hemiWalletClient, | ||
token, | ||
}), | ||
).rejects.toThrow('not-enough-balance') | ||
}) | ||
|
||
it('should call stakeToken if all conditions are met', async function () { | ||
getErc20TokenBalance.mockResolvedValue(parseUnits('10', token.decimals)) | ||
await stake({ | ||
amount: '1', | ||
from: zeroAddress, | ||
hemiPublicClient, | ||
hemiWalletClient, | ||
token, | ||
}) | ||
expect(hemiWalletClient.stakeToken).toHaveBeenCalledWith({ | ||
amount: parseUnits('1', token.decimals), | ||
from: zeroAddress, | ||
token, | ||
}) | ||
}) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
import { | ||
type HemiPublicClient, | ||
type HemiWalletClient, | ||
} from 'hooks/useHemiClient' | ||
import { EvmToken } from 'types/token' | ||
import { getErc20TokenBalance } from 'utils/token' | ||
import { type Address, Chain, parseUnits } from 'viem' | ||
|
||
/** | ||
* Determines if a user is able to stake or unstake a token | ||
* | ||
* @param params All the parameters needed to determine if a user can stake or unstake a token | ||
* @param amount The amount of tokens to stake/unstake | ||
* @param balance The balance the user has of the token | ||
* @param connectedChainId The chain Id the user is connected to | ||
* @param token The token to stake/unstake | ||
*/ | ||
export const canSubmit = function ({ | ||
amount, | ||
balance, | ||
connectedChainId, | ||
token, | ||
}: { | ||
amount: bigint | ||
balance: bigint | ||
connectedChainId: Chain['id'] | ||
token: EvmToken | ||
}) { | ||
if (amount <= 0) { | ||
return { error: 'amount-less-equal-than-0' } | ||
} | ||
// this chain Id comes from the hemi client. It verifies that the token | ||
// is on expected hemi chain (hemi / testnet). | ||
if (connectedChainId !== token.chainId) { | ||
return { error: 'wrong-chain' } | ||
} | ||
if (balance <= 0) { | ||
return { error: 'not-enough-balance' } | ||
} | ||
if (amount > balance) { | ||
return { error: 'amount-larger-than-balance' } | ||
} | ||
return {} | ||
} | ||
|
||
const validateOperation = async function ({ | ||
amount, | ||
from, | ||
hemiPublicClient, | ||
token, | ||
}: { | ||
amount: bigint | ||
from: Address | ||
hemiPublicClient: HemiPublicClient | ||
token: EvmToken | ||
}) { | ||
const balance = await getErc20TokenBalance({ | ||
address: from, | ||
// It works in runtime, but Typescript fails to interpret HemiPublicClient as a Client. | ||
// I've seen that the typings change in future viem's version, so this may be soon fixed | ||
// @ts-expect-error hemiPublicClient is Client | ||
client: hemiPublicClient, | ||
token, | ||
}) | ||
const { error } = canSubmit({ | ||
amount, | ||
balance, | ||
connectedChainId: hemiPublicClient.chain.id, | ||
token, | ||
}) | ||
if (error) { | ||
throw new Error(error) | ||
} | ||
} | ||
|
||
/** | ||
* Stakes an amount of a token in Hemi. | ||
* @param params All the parameters needed to determine if a user can stake a token | ||
* @param amount The amount of tokens to stake | ||
* @param from The address of the user that will stake | ||
* @param hemiPublicClient Hemi public client for read-only calls | ||
* @param hemiWalletClient Hemi Wallet client for signing transactions | ||
* @param token The token to stake | ||
*/ | ||
export const stake = async function ({ | ||
amount, | ||
from, | ||
hemiPublicClient, | ||
hemiWalletClient, | ||
token, | ||
}: { | ||
amount: string | ||
from: Address | ||
hemiPublicClient: HemiPublicClient | ||
hemiWalletClient: HemiWalletClient | ||
token: EvmToken | ||
}) { | ||
const amountUnits = parseUnits(amount, token.decimals) | ||
await validateOperation({ | ||
amount: amountUnits, | ||
from, | ||
hemiPublicClient, | ||
token, | ||
}) | ||
|
||
return hemiWalletClient.stakeToken({ | ||
amount: amountUnits, | ||
from, | ||
token, | ||
}) | ||
} | ||
|
||
/** | ||
* Unstakes an amount of a token in Hemi. | ||
* @param params All the parameters needed to determine if a user can unstake a token | ||
* @param amount The amount of tokens to stake | ||
* @param from The address of the user that will unstake | ||
* @param hemiPublicClient Hemi public client for read-only calls | ||
* @param hemiWalletClient Hemi Wallet client for signing transactions | ||
* @param token The token to stake | ||
*/ | ||
export const unstake = async function ({ | ||
amount, | ||
from, | ||
hemiPublicClient, | ||
hemiWalletClient, | ||
token, | ||
}: { | ||
amount: string | ||
from: Address | ||
hemiPublicClient: HemiPublicClient | ||
hemiWalletClient: HemiWalletClient | ||
token: EvmToken | ||
}) { | ||
const amountUnits = parseUnits(amount, token.decimals) | ||
// Here I am assuming that when the user stakes, we get a staked token | ||
// that can later be used to withdraw. That's why we need to check the balance of this staked token inside "validate operation" | ||
await validateOperation({ | ||
amount: amountUnits, | ||
from, | ||
hemiPublicClient, | ||
token, | ||
}) | ||
return hemiWalletClient.unstakeToken({ | ||
amount: amountUnits, | ||
from, | ||
token, | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters