mars-v2-frontend/src/store/slices/broadcast.ts
Bob van der Helm dee7f5936b
🐛 UNSAFE_COMPONENT error (#439)
* 🐛 UNSAFE_COMPONENT error

* 🐛 fix unit tests for react-helmet-async
2023-09-07 10:37:49 +02:00

536 lines
16 KiB
TypeScript

import { MsgExecuteContract } from '@delphi-labs/shuttle-react'
import { isMobile } from 'react-device-detect'
import { GetState, SetState } from 'zustand'
import { ENV } from 'constants/env'
import { Store } from 'store'
import { BNCoin } from 'types/classes/BNCoin'
import { ExecuteMsg as AccountNftExecuteMsg } from 'types/generated/mars-account-nft/MarsAccountNft.types'
import {
Action,
ActionCoin,
Action as CreditManagerAction,
ExecuteMsg as CreditManagerExecuteMsg,
} from 'types/generated/mars-credit-manager/MarsCreditManager.types'
import { getAssetByDenom, getAssetBySymbol } from 'utils/assets'
import { generateErrorMessage, getSingleValueFromBroadcastResult } from 'utils/broadcast'
import checkAutoLendEnabled from 'utils/checkAutoLendEnabled'
import { defaultFee } from 'utils/constants'
import { formatAmountWithSymbol } from 'utils/formatters'
import getTokenOutFromSwapResponse from 'utils/getTokenOutFromSwapResponse'
import { BN } from 'utils/helpers'
function generateExecutionMessage(
sender: string | undefined = '',
contract: string,
msg: CreditManagerExecuteMsg | AccountNftExecuteMsg,
funds: Coin[],
) {
return new MsgExecuteContract({
sender,
contract,
msg,
funds,
})
}
export default function createBroadcastSlice(
set: SetState<Store>,
get: GetState<Store>,
): BroadcastSlice {
const handleResponseMessages = (
response: BroadcastResult,
successMessage: string,
errorMessage?: string,
) => {
if (response.result?.response.code === 0) {
set({
toast: {
message: successMessage,
hash: response.result.hash,
},
})
} else {
set({
toast: {
message: generateErrorMessage(response, errorMessage),
isError: true,
hash: response.result?.hash,
},
})
}
}
const getEstimatedFee = async (messages: MsgExecuteContract[]) => {
if (!get().client) {
return defaultFee
}
try {
const simulateResult = await get().client?.simulate({
messages,
wallet: get().client?.connectedWallet,
})
if (simulateResult) {
const { success, fee } = simulateResult
if (success) {
return {
amount: fee ? fee.amount : [],
gas: BN(fee ? fee.gas : 0).toFixed(0),
}
}
}
throw 'Simulation failed'
} catch (ex) {
return defaultFee
}
}
return {
toast: null,
borrow: async (options: { accountId: string; coin: BNCoin; borrowToWallet: boolean }) => {
const borrowAction: Action = { borrow: options.coin.toCoin() }
const withdrawAction: Action = { withdraw: options.coin.toActionCoin() }
const actions = options.borrowToWallet ? [borrowAction, withdrawAction] : [borrowAction]
const msg: CreditManagerExecuteMsg = {
update_credit_account: {
account_id: options.accountId,
actions,
},
}
if (
!options.borrowToWallet &&
checkAutoLendEnabled(options.accountId) &&
getAssetByDenom(options.coin.denom)?.isAutoLendEnabled
) {
msg.update_credit_account.actions.push({
lend: { denom: options.coin.denom, amount: 'account_balance' },
})
}
const response = await get().executeMsg({
messages: [generateExecutionMessage(get().address, ENV.ADDRESS_CREDIT_MANAGER, msg, [])],
})
handleResponseMessages(
response,
`Borrowed ${formatAmountWithSymbol(options.coin.toCoin())} to ${
options.borrowToWallet ? 'Wallet' : `Credit Account ${options.accountId}`
}`,
)
return !!response.result
},
createAccount: async () => {
const msg: CreditManagerExecuteMsg = {
create_credit_account: 'default',
}
const response = await get().executeMsg({
messages: [generateExecutionMessage(get().address, ENV.ADDRESS_CREDIT_MANAGER, msg, [])],
})
if (response.result && !response.error) {
set({ createAccountModal: false })
const id = getSingleValueFromBroadcastResult(response.result, 'wasm', 'token_id')
set({
fundAccountModal: true,
toast: { message: `Credit Account ${id} created`, hash: response.result.hash },
})
return id
} else {
set({
createAccountModal: false,
toast: {
message: generateErrorMessage(response),
hash: response?.result?.hash,
isError: true,
},
})
return null
}
},
deleteAccount: async (options: { accountId: string; lends: BNCoin[] }) => {
const reclaimMsg = options.lends.map((coin) => {
return {
reclaim: coin.toActionCoin(true),
}
})
const refundMessage: CreditManagerExecuteMsg = {
update_credit_account: {
account_id: options.accountId,
actions: [...reclaimMsg, { refund_all_coin_balances: {} }],
},
}
const burnMessage: AccountNftExecuteMsg = {
burn: {
token_id: options.accountId,
},
}
const response = await get().executeMsg({
messages: [
generateExecutionMessage(get().address, ENV.ADDRESS_CREDIT_MANAGER, refundMessage, []),
generateExecutionMessage(get().address, ENV.ADDRESS_ACCOUNT_NFT, burnMessage, []),
],
})
handleResponseMessages(response, `Credit Account ${options.accountId} deleted`)
return !!response.result
},
claimRewards: (options: { accountId: string }) => {
const msg: CreditManagerExecuteMsg = {
update_credit_account: {
account_id: options.accountId,
actions: [
{
claim_rewards: {},
},
],
},
}
const messages = [
generateExecutionMessage(get().address, ENV.ADDRESS_CREDIT_MANAGER, msg, []),
]
const estimateFee = () => getEstimatedFee(messages)
const execute = async () => {
const response = await get().executeMsg({
messages,
})
const successMessage = `Claimed rewards for, ${options.accountId}`
handleResponseMessages(response, successMessage)
return !!response.result
}
return { estimateFee, execute }
},
deposit: async (options: { accountId: string; coins: BNCoin[]; lend: boolean }) => {
const msg: CreditManagerExecuteMsg = {
update_credit_account: {
account_id: options.accountId,
actions: options.coins.map((coin) => ({
deposit: coin.toCoin(),
})),
},
}
if (options.lend) {
msg.update_credit_account.actions.push(
...options.coins
.filter((coin) => getAssetByDenom(coin.denom)?.isAutoLendEnabled)
.map((coin) => ({ lend: coin.toActionCoin(options.lend) })),
)
}
const funds = options.coins.map((coin) => coin.toCoin())
const response = await get().executeMsg({
messages: [generateExecutionMessage(get().address, ENV.ADDRESS_CREDIT_MANAGER, msg, funds)],
})
const depositString = options.coins
.map((coin) => formatAmountWithSymbol(coin.toCoin()))
.join(' and ')
handleResponseMessages(
response,
`Deposited ${options.lend ? 'and lent ' : ''}${depositString} to Credit Account ${
options.accountId
}`,
)
return !!response.result
},
unlock: async (options: { accountId: string; vault: DepositedVault; amount: string }) => {
const msg: CreditManagerExecuteMsg = {
update_credit_account: {
account_id: options.accountId,
actions: [
{
request_vault_unlock: {
vault: { address: options.vault.address },
amount: options.amount,
},
},
],
},
}
const response = await get().executeMsg({
messages: [generateExecutionMessage(get().address, ENV.ADDRESS_CREDIT_MANAGER, msg, [])],
})
handleResponseMessages(response, `Requested unlock for ${options.vault.name}`)
return !!response.result
},
withdrawFromVaults: async (options: { accountId: string; vaults: DepositedVault[] }) => {
const actions: CreditManagerAction[] = []
options.vaults.forEach((vault) => {
if (vault.unlockId)
actions.push({
exit_vault_unlocked: {
id: vault.unlockId,
vault: { address: vault.address },
},
})
})
const msg: CreditManagerExecuteMsg = {
update_credit_account: {
account_id: options.accountId,
actions,
},
}
if (checkAutoLendEnabled(options.accountId)) {
for (const vault of options.vaults) {
for (const symbol of Object.values(vault.symbols)) {
const asset = getAssetBySymbol(symbol)
if (asset?.isAutoLendEnabled) {
msg.update_credit_account.actions.push({
lend: { denom: asset.denom, amount: 'account_balance' },
})
}
}
}
}
const response = await get().executeMsg({
messages: [generateExecutionMessage(get().address, ENV.ADDRESS_CREDIT_MANAGER, msg, [])],
})
const vaultsString = options.vaults.length === 1 ? 'vault' : 'vaults'
handleResponseMessages(
response,
`You successfully withdrew ${options.vaults.length} unlocked ${vaultsString} to your account`,
)
return !!response.result
},
depositIntoVault: async (options: { accountId: string; actions: Action[] }) => {
const msg: CreditManagerExecuteMsg = {
update_credit_account: {
account_id: options.accountId,
actions: options.actions,
},
}
const response = await get().executeMsg({
messages: [generateExecutionMessage(get().address, ENV.ADDRESS_CREDIT_MANAGER, msg, [])],
})
handleResponseMessages(response, `Deposited into vault`)
return !!response.result
},
withdraw: async (options: {
accountId: string
coins: Array<{ coin: BNCoin; isMax?: boolean }>
borrow: BNCoin[]
reclaims: ActionCoin[]
}) => {
const reclaimActions = options.reclaims.map((coin) => ({
reclaim: coin,
}))
const withdrawActions = options.coins.map(({ coin, isMax }) => ({
withdraw: coin.toActionCoin(isMax),
}))
const borrowActions = options.borrow.map((coin) => ({
borrow: coin.toCoin(),
}))
const msg: CreditManagerExecuteMsg = {
update_credit_account: {
account_id: options.accountId,
actions: [...reclaimActions, ...borrowActions, ...withdrawActions],
},
}
const response = await get().executeMsg({
messages: [generateExecutionMessage(get().address, ENV.ADDRESS_CREDIT_MANAGER, msg, [])],
})
const withdrawString = options.coins
.map(({ coin }) => formatAmountWithSymbol(coin.toCoin()))
.join('and ')
handleResponseMessages(
response,
`Withdrew ${withdrawString} from Credit Account ${options.accountId}`,
)
return !!response.result
},
repay: async (options: {
accountId: string
coin: BNCoin
accountBalance?: boolean
lend?: BNCoin
}) => {
const actions: Action[] = [
{
repay: {
coin: options.coin.toActionCoin(options.accountBalance),
},
},
]
if (options.lend && options.lend.amount.isGreaterThan(0))
actions.unshift({ reclaim: options.lend.toActionCoin() })
const msg: CreditManagerExecuteMsg = {
update_credit_account: {
account_id: options.accountId,
actions,
},
}
const response = await get().executeMsg({
messages: [generateExecutionMessage(get().address, ENV.ADDRESS_CREDIT_MANAGER, msg, [])],
})
handleResponseMessages(
response,
`Repayed ${formatAmountWithSymbol(options.coin.toCoin())} to Credit Account ${
options.accountId
}`,
)
return !!response.result
},
lend: async (options: { accountId: string; coin: BNCoin; isMax?: boolean }) => {
const msg: CreditManagerExecuteMsg = {
update_credit_account: {
account_id: options.accountId,
actions: [
{
lend: options.coin.toActionCoin(options.isMax),
},
],
},
}
const response = await get().executeMsg({
messages: [generateExecutionMessage(get().address, ENV.ADDRESS_CREDIT_MANAGER, msg, [])],
})
handleResponseMessages(
response,
`Successfully lent ${formatAmountWithSymbol(options.coin.toCoin())}`,
)
return !!response.result
},
reclaim: async (options: { accountId: string; coin: BNCoin; isMax?: boolean }) => {
const msg: CreditManagerExecuteMsg = {
update_credit_account: {
account_id: options.accountId,
actions: [
{
reclaim: options.coin.toActionCoin(options.isMax),
},
],
},
}
const response = await get().executeMsg({
messages: [generateExecutionMessage(get().address, ENV.ADDRESS_CREDIT_MANAGER, msg, [])],
})
handleResponseMessages(
response,
`Successfully withdrew ${formatAmountWithSymbol(options.coin.toCoin())}`,
)
return !!response.result
},
swap: (options: {
accountId: string
coinIn: BNCoin
reclaim?: BNCoin
borrow?: BNCoin
denomOut: string
slippage: number
isMax?: boolean
}) => {
const msg: CreditManagerExecuteMsg = {
update_credit_account: {
account_id: options.accountId,
actions: [
...(options.reclaim ? [{ reclaim: options.reclaim.toActionCoin() }] : []),
...(options.borrow ? [{ borrow: options.borrow.toCoin() }] : []),
{
swap_exact_in: {
coin_in: options.coinIn.toActionCoin(options.isMax),
denom_out: options.denomOut,
slippage: options.slippage.toString(),
},
},
],
},
}
if (
checkAutoLendEnabled(options.accountId) &&
getAssetByDenom(options.denomOut)?.isAutoLendEnabled
) {
msg.update_credit_account.actions.push({
lend: { denom: options.denomOut, amount: 'account_balance' },
})
}
const messages = [
generateExecutionMessage(get().address, ENV.ADDRESS_CREDIT_MANAGER, msg, []),
]
const estimateFee = () => getEstimatedFee(messages)
const execute = async () => {
const response = await get().executeMsg({
messages,
})
const coinOut = getTokenOutFromSwapResponse(response, options.denomOut)
const successMessage = `Swapped ${formatAmountWithSymbol(
options.coinIn.toCoin(),
)} for ${formatAmountWithSymbol(coinOut)}`
handleResponseMessages(response, successMessage)
return !!response.result
}
return { estimateFee, execute }
},
executeMsg: async (options: { messages: MsgExecuteContract[] }): Promise<BroadcastResult> => {
try {
const client = get().client
if (!client) return { error: 'no client detected' }
const fee = await getEstimatedFee(options.messages)
const broadcastOptions = {
messages: options.messages,
feeAmount: fee.amount[0].amount,
gasLimit: fee.gas,
memo: undefined,
wallet: client.connectedWallet,
mobile: isMobile,
}
const result = await client.broadcast(broadcastOptions)
if (result.hash) {
return { result }
}
return {
result: undefined,
error: 'Transaction failed',
}
} catch (e: unknown) {
const error = typeof e === 'string' ? e : 'Transaction failed'
return { result: undefined, error }
}
},
}
}