diff --git a/components/admins/modals/__tests__/validatorModal.test.tsx b/components/admins/modals/__tests__/validatorModal.test.tsx index 2715d6f3..fc66e27a 100644 --- a/components/admins/modals/__tests__/validatorModal.test.tsx +++ b/components/admins/modals/__tests__/validatorModal.test.tsx @@ -48,7 +48,7 @@ describe('ValidatorDetailsModal Component', () => { }); }); - test('enables update button when input is valid', async () => { + test('enables upgrade button when input is valid', async () => { renderWithProps(); const input = screen.getByPlaceholderText('1000'); fireEvent.change(input, { target: { value: '2000' } }); @@ -58,7 +58,7 @@ describe('ValidatorDetailsModal Component', () => { }); }); - test('disables update button when input is invalid', async () => { + test('disables upgrade button when input is invalid', async () => { renderWithProps(); const input = screen.getByPlaceholderText('1000'); fireEvent.change(input, { target: { value: '-1' } }); @@ -82,7 +82,7 @@ describe('ValidatorDetailsModal Component', () => { }); }); - test('shows warning message for unsafe power update', async () => { + test('shows warning message for unsafe power upgrade', async () => { renderWithProps(); const input = screen.getByPlaceholderText('1000'); fireEvent.change(input, { target: { value: '9000' } }); diff --git a/components/bank/components/__tests__/historyBox.test.tsx b/components/bank/components/__tests__/historyBox.test.tsx index 48f35a22..cea7552e 100644 --- a/components/bank/components/__tests__/historyBox.test.tsx +++ b/components/bank/components/__tests__/historyBox.test.tsx @@ -1,8 +1,8 @@ import { test, expect, afterEach, describe, mock, jest } from 'bun:test'; import { screen, cleanup, fireEvent } from '@testing-library/react'; -import { HistoryBox } from '../historyBox'; import { renderWithChainProvider } from '@/tests/render'; import { mockTransactions } from '@/tests/mock'; +import { HistoryBox } from '../historyBox'; import matchers from '@testing-library/jest-dom/matchers'; expect.extend(matchers); @@ -52,7 +52,7 @@ describe('HistoryBox', () => { ); }); - test('displays transactions', () => { + test('displays transactions as `address1`', () => { renderWithChainProvider( { totalPages={2} /> ); - expect(screen.getByText('Sent')).toBeInTheDocument(); - expect(screen.getByText('Received')).toBeInTheDocument(); - - const minted = screen.getAllByText('Minted'); - const burned = screen.getAllByText('Burned'); + expect(screen.getByText(/You sent/i)).toBeInTheDocument(); + expect(screen.getByText(/You received/i)).toBeInTheDocument(); + expect(screen.getAllByText(/You were burned/i)).toHaveLength(2); + expect(screen.getAllByText(/You minted/i)).toHaveLength(2); + expect(screen.getAllByText(/You were minted/i)).toHaveLength(4); + }); - expect(minted.length).toBe(6); - expect(burned.length).toBe(2); + test('displays transactions as `address2`', () => { + renderWithChainProvider( + + ); + expect(screen.getByText(/You sent/i)).toBeInTheDocument(); + expect(screen.getByText(/You received/i)).toBeInTheDocument(); + expect(screen.getAllByText(/You burned/i)).toHaveLength(2); + expect(screen.getAllByText(/You were minted/i)).toHaveLength(2); + expect(screen.getAllByText(/You minted/i)).toHaveLength(4); }); test('opens modal when clicking on a transaction', () => { @@ -83,7 +97,7 @@ describe('HistoryBox', () => { /> ); - const transactionElement = screen.getByText('Sent').closest('div[role="button"]'); + const transactionElement = screen.getByText(/You sent/i).closest('div[role="button"]'); if (transactionElement) { fireEvent.click(transactionElement); @@ -101,16 +115,16 @@ describe('HistoryBox', () => { totalPages={2} /> ); - expect(screen.queryByText('-1.00QT TOKEN')).toBeInTheDocument(); // Send - expect(screen.queryByText('+2.00Q TOKEN')).toBeInTheDocument(); // Receive - expect(screen.queryByText('+3.00T TOKEN')).toBeInTheDocument(); // Mint - expect(screen.queryByText('-1.20B TOKEN')).toBeInTheDocument(); // Burn - expect(screen.queryByText('+5.00M TOKEN')).toBeInTheDocument(); // Payout - expect(screen.queryByText('-2.1 TOKEN')).toBeInTheDocument(); // Burn held balance - expect(screen.queryByText('+2.3 TOKEN')).toBeInTheDocument(); // Payout - expect(screen.queryByText('+2.4 TOKEN')).toBeInTheDocument(); // Payout - expect(screen.queryByText('+2.5 TOKEN')).toBeInTheDocument(); // Payout - expect(screen.queryByText('+2.6 TOKEN')).toBeInTheDocument(); // Payout + expect(screen.queryByText('1.00QT TOKEN')).toBeInTheDocument(); // Send + expect(screen.queryByText('2.00Q TOKEN')).toBeInTheDocument(); // Receive + expect(screen.queryByText('3.00T TOKEN')).toBeInTheDocument(); // Mint + expect(screen.queryByText('1.20B TOKEN')).toBeInTheDocument(); // Burn + expect(screen.queryByText('5.00M TOKEN')).toBeInTheDocument(); // Payout + expect(screen.queryByText('2.1 TOKEN')).toBeInTheDocument(); // Burn held balance + expect(screen.queryByText('2.3 TOKEN')).toBeInTheDocument(); // Payout + expect(screen.queryByText('2.4 TOKEN')).toBeInTheDocument(); // Payout + expect(screen.queryByText('2.5 TOKEN')).toBeInTheDocument(); // Payout + expect(screen.queryByText('2.6 TOKEN')).toBeInTheDocument(); // Payout }); test('displays loading state', () => { diff --git a/components/bank/components/historyBox.tsx b/components/bank/components/historyBox.tsx index 127cdd7b..697c3b96 100644 --- a/components/bank/components/historyBox.tsx +++ b/components/bank/components/historyBox.tsx @@ -1,48 +1,17 @@ -import React, { useMemo, useState, useEffect } from 'react'; -import { TruncatedAddressWithCopy } from '@/components/react/addressCopy'; +import React, { useState } from 'react'; +import { TransactionAmount, TxMessage } from '../types'; +import { shiftDigits, formatLargeNumber, formatDenom } from '@/utils'; +import { getHandler } from '@/components/bank/handlers/handlerRegistry'; +import { MetadataSDKType } from '@liftedinit/manifestjs/dist/codegen/cosmos/bank/v1beta1/bank'; +import { useTokenFactoryDenomsMetadata } from '@/hooks'; import TxInfoModal from '../modals/txInfo'; -import { denomToAsset, shiftDigits, truncateString } from '@/utils'; -import { BurnIcon, DenomImage, formatDenom, MintIcon } from '@/components'; -import { HistoryTxType, useTokenFactoryDenomsMetadata } from '@/hooks'; -import { ReceiveIcon, SendIcon } from '@/components/icons'; - -import useIsMobile from '@/hooks/useIsMobile'; -import env from '@/config/env'; - -interface Transaction { - tx_type: HistoryTxType; - from_address: string; - to_address: string; - amount: Array<{ amount: string; denom: string }>; -} export interface TransactionGroup { tx_hash: string; block_number: number; formatted_date: string; + fee?: TransactionAmount; memo?: string; - data: Transaction; -} - -function formatLargeNumber(num: number): string { - const quintillion = 1e18; - const quadrillion = 1e15; - const trillion = 1e12; - const billion = 1e9; - const million = 1e6; - - if (num >= quintillion) { - return `${(num / quintillion).toFixed(2)}QT`; - } else if (num >= quadrillion) { - return `${(num / quadrillion).toFixed(2)}Q`; - } else if (num >= trillion) { - return `${(num / trillion).toFixed(2)}T`; - } else if (num >= billion) { - return `${(num / billion).toFixed(2)}B`; - } else if (num >= million) { - return `${(num / million).toFixed(2)}M`; - } - return num.toLocaleString(); } export function HistoryBox({ @@ -58,12 +27,12 @@ export function HistoryBox({ skeletonGroupCount, skeletonTxCount, isGroup, -}: { +}: Readonly<{ isLoading: boolean; address: string; currentPage: number; setCurrentPage: React.Dispatch>; - sendTxs: TransactionGroup[]; + sendTxs: TxMessage[]; totalPages: number; txLoading: boolean; isError: boolean; @@ -71,12 +40,11 @@ export function HistoryBox({ skeletonGroupCount: number; skeletonTxCount: number; isGroup?: boolean; -}) { - const [selectedTx, setSelectedTx] = useState(null); - - const isLoading = initialLoading || txLoading; +}>) { + const [selectedTx, setSelectedTx] = useState(null); + const { metadatas, isMetadatasLoading } = useTokenFactoryDenomsMetadata(); - const { metadatas } = useTokenFactoryDenomsMetadata(); + const isLoading = initialLoading || txLoading || isMetadatasLoading; function formatDateShort(dateString: string): string { const date = new Date(dateString); @@ -87,73 +55,19 @@ export function HistoryBox({ }); } - function getTransactionIcon(tx: TransactionGroup, address: string) { - if (tx.data.tx_type === HistoryTxType.SEND) { - return tx.data.from_address === address ? : ; - } else if (tx.data.tx_type === HistoryTxType.MINT || tx.data.tx_type === HistoryTxType.PAYOUT) { - return ( - - ); - } else if ( - tx.data.tx_type === HistoryTxType.BURN || - tx.data.tx_type === HistoryTxType.BURN_HELD_BALANCE - ) { - return ( - - ); - } - return null; - } - - // Get the history message based on the transaction type - function getTransactionMessage(tx: TransactionGroup, address: string) { - if (tx.data.tx_type === HistoryTxType.SEND) { - return tx.data.from_address === address ? 'Sent' : 'Received'; - } else if (tx.data.tx_type === HistoryTxType.MINT || tx.data.tx_type === HistoryTxType.PAYOUT) { - return 'Minted'; - } else if ( - tx.data.tx_type === HistoryTxType.BURN || - tx.data.tx_type === HistoryTxType.BURN_HELD_BALANCE - ) { - return 'Burned'; - } - return 'Unsupported'; - } - - // Get the transaction direction based on the transaction type - function getTransactionPlusMinus(tx: TransactionGroup, address: string) { - if (tx.data.tx_type === HistoryTxType.SEND) { - return tx.data.from_address === address ? '-' : '+'; - } else if (tx.data.tx_type === HistoryTxType.MINT || tx.data.tx_type === HistoryTxType.PAYOUT) { - return '+'; - } else if ( - tx.data.tx_type === HistoryTxType.BURN || - tx.data.tx_type === HistoryTxType.BURN_HELD_BALANCE - ) { - return '-'; - } - return '!!'; + function getTransactionIcon(tx: TxMessage, address: string) { + const handler = getHandler(tx.type); + const { icon: IconComponent } = handler(tx, address); + return ; } - // Get the transaction color based on the transaction type and direction - function getTransactionColor(tx: TransactionGroup, address: string) { - if (tx.data.tx_type === HistoryTxType.SEND) { - return tx.data.from_address === address ? 'text-red-500' : 'text-green-500'; - } else if (tx.data.tx_type === HistoryTxType.MINT || tx.data.tx_type === HistoryTxType.PAYOUT) { - return 'text-green-500'; - } else if ( - tx.data.tx_type === HistoryTxType.BURN || - tx.data.tx_type === HistoryTxType.BURN_HELD_BALANCE - ) { - return 'text-red-500'; - } - return null; + function getTransactionMessage(tx: TxMessage, address: string, metadata?: MetadataSDKType[]) { + const handler = getHandler(tx.type); + return handler(tx, address, metadata).message; } return ( -
+
{isLoading ? (
@@ -176,7 +90,7 @@ export function HistoryBox({
-
+
))}
@@ -201,134 +115,60 @@ export function HistoryBox({

) : ( -
+
{sendTxs?.slice(0, skeletonTxCount).map((tx, index) => (
{ setSelectedTx(tx); - (document?.getElementById(`tx_modal_info`) as HTMLDialogElement)?.showModal(); + (document?.getElementById('tx_modal_info') as HTMLDialogElement)?.showModal(); }} > -
-
+
+
{getTransactionIcon(tx, address)}
- - {tx.data.amount.map((amt, index) => { - const assetInfo = denomToAsset(env.chain, amt.denom); - let metadata = metadatas?.metadatas.find(m => m.base === amt.denom); - - if (amt.denom.startsWith('ibc/')) { - if (assetInfo) { - metadata = { - description: assetInfo?.description ?? '', - denom_units: - assetInfo?.denom_units?.map(unit => ({ - ...unit, - aliases: unit.aliases || [], - })) ?? [], - base: assetInfo?.base ?? '', - display: assetInfo?.display ?? '', - name: assetInfo?.name ?? '', - symbol: assetInfo?.symbol ?? '', - uri: assetInfo?.logo_URIs?.svg ?? assetInfo?.logo_URIs?.png ?? '', - uri_hash: assetInfo?.logo_URIs?.svg ?? assetInfo?.logo_URIs?.png ?? '', - }; - } else { - // assetInfo is undefined - metadata = { - description: '', - denom_units: [], - base: '', - display: '', - name: '', - symbol: '', - uri: '', - uri_hash: '', - }; - } - } - - return ; - })} -
-
-

- {getTransactionMessage(tx, address)} -

-

- {tx.data.amount.map((amt, index) => { - const metadata = metadatas?.metadatas.find(m => m.base === amt.denom); - let display = metadata?.display ?? metadata?.symbol ?? ''; - - if (amt.denom.startsWith('ibc/')) { - const assetInfo = denomToAsset(env.chain, amt.denom); - if (assetInfo?.traces && assetInfo.traces.length > 0) { - if (assetInfo.traces[0].counterparty?.base_denom) { - display = assetInfo.traces[0].counterparty.base_denom.slice(1); - } - } - } - - return metadata?.display?.startsWith('factory') - ? metadata?.display?.split('/').pop()?.toUpperCase() - : display.length > 4 - ? display.slice(0, 4).toUpperCase() + '...' - : display.toUpperCase(); - })} -

+

+ {formatDateShort(tx.timestamp)} +

+
+ + {getTransactionMessage(tx, address, metadatas?.metadatas)} +
-
e.stopPropagation()} - > - {tx.data.from_address.startsWith('manifest1') ? ( - - ) : ( -
- {tx.data.from_address} + {tx.message_index < 10000 ? ( + tx.sender === address ? ( +
+ Incl.:{' '} + {tx.fee && + formatLargeNumber( + Number(shiftDigits(tx.fee.amount?.[0]?.amount, -6)) + ) + + ' ' + + formatDenom(tx.fee.amount?.[0]?.denom)}{' '} + fee
- )} -
+ ) : null + ) : ( +
+ Fee incl. in proposal #{tx.proposal_ids} execution +
+ )}
-
-

- {formatDateShort(tx.formatted_date)} -

-

- {getTransactionPlusMinus(tx, address)} - {tx.data.amount - .map(amt => { - const metadata = metadatas?.metadatas.find(m => m.base === amt.denom); - const exponent = Number(metadata?.denom_units[1]?.exponent) || 6; - const amount = Number(shiftDigits(amt.amount, -exponent)); - let baseDenom = formatDenom(amt.denom); - - if (amt.denom.startsWith('ibc/')) { - const assetInfo = denomToAsset(env.chain, amt.denom); - if (assetInfo?.traces && assetInfo.traces.length > 0) { - if (assetInfo.traces[0].counterparty?.base_denom) { - baseDenom = assetInfo.traces[0].counterparty.base_denom.slice(1); - } - } - } - - return `${formatLargeNumber(amount)} ${baseDenom.toUpperCase()}`; - }) - .join(', ')} -

-
+ {/* Example of placing date/ID on the right side on larger screens: +
+ Tx ID: {tx.id} +
+ */}
))}
@@ -337,7 +177,7 @@ export function HistoryBox({ )} {totalPages > 1 && ( -
+
@@ -392,7 +232,7 @@ export function HistoryBox({
)} - +
); } diff --git a/components/bank/components/index.ts b/components/bank/components/index.ts index ea097264..615509d3 100644 --- a/components/bank/components/index.ts +++ b/components/bank/components/index.ts @@ -1,13 +1,3 @@ export * from './sendBox'; export * from './tokenList'; export * from './historyBox'; - -export function formatDenom(denom: string): string { - const cleanDenom = denom.replace(/^factory\/[^/]+\//, ''); - - if (cleanDenom.startsWith('u')) { - return cleanDenom.slice(1).toUpperCase(); - } - - return cleanDenom; -} diff --git a/components/bank/handlers/bank/index.ts b/components/bank/handlers/bank/index.ts new file mode 100644 index 00000000..a23f3999 --- /dev/null +++ b/components/bank/handlers/bank/index.ts @@ -0,0 +1,2 @@ +export * from './msgSendHandler'; +export * from './msgMultiSendHandler'; diff --git a/components/bank/handlers/bank/msgMultiSendHandler.tsx b/components/bank/handlers/bank/msgMultiSendHandler.tsx new file mode 100644 index 00000000..b94f92bc --- /dev/null +++ b/components/bank/handlers/bank/msgMultiSendHandler.tsx @@ -0,0 +1,73 @@ +import { MsgMultiSend } from '@liftedinit/manifestjs/dist/codegen/cosmos/bank/v1beta1/tx'; +import { createSenderReceiverHandler } from '@/components/bank/handlers/createSenderReceiverHandler'; +import { registerHandler } from '@/components/bank/handlers/handlerRegistry'; +import { BankIcon } from '@/components/icons/BankIcon'; +import { format } from 'react-string-format'; +import { TruncatedAddressWithCopy } from '@/components/react/addressCopy'; +import { MetadataSDKType } from '@liftedinit/manifestjs/dist/codegen/cosmos/bank/v1beta1/bank'; +import { formatAmount, formatDenom, formatLargeNumber } from '@/utils'; +import React from 'react'; + +const createMessage = ( + template: string, + amount: string, + denom: string, + numReceivers: number, + color: string, + metadata?: MetadataSDKType[], + sender?: string +) => { + const formattedAmount = formatLargeNumber(formatAmount(amount, denom, metadata)); + const formattedDenom = formatDenom(denom); + const coloredAmount = ( + + {formattedAmount} {formattedDenom} + + ); + const coloredDenom = {formattedDenom}; + const message = format( + template, + coloredAmount, + numReceivers, + coloredDenom, + sender ? : 'an unknown address' + ); + return {message}; +}; + +export const MsgMultiSendHandler = createSenderReceiverHandler({ + iconSender: BankIcon, + successSender: (tx, _, metadata) => { + return createMessage( + 'You sent {0} equally divided between {1} addresses', + tx.metadata?.inputs?.[0]?.coins?.[0]?.amount, + tx.metadata?.inputs?.[0]?.coins?.[0]?.denom, + tx.metadata?.outputs?.length, + 'red', + metadata + ); + }, + failSender: (tx, _, metadata) => { + return createMessage( + 'You failed to send {0} equally divided between {1} addresses', + tx.metadata?.inputs?.[0]?.coins?.[0]?.amount, + tx.metadata?.inputs?.[0]?.coins?.[0]?.denom, + tx.metadata?.outputs?.length, + 'red', + metadata + ); + }, + successReceiver: (tx, _, metadata) => { + return createMessage( + 'You received {2} tokens from {3}', + tx.metadata?.inputs?.[0]?.coins?.[0]?.amount, + tx.metadata?.inputs?.[0]?.coins?.[0]?.denom, + tx.metadata?.outputs?.length, + 'green', + metadata, + tx.sender + ); + }, +}); + +registerHandler(MsgMultiSend.typeUrl, MsgMultiSendHandler); diff --git a/components/bank/handlers/bank/msgSendHandler.tsx b/components/bank/handlers/bank/msgSendHandler.tsx new file mode 100644 index 00000000..42928442 --- /dev/null +++ b/components/bank/handlers/bank/msgSendHandler.tsx @@ -0,0 +1,41 @@ +import { BankIcon } from '@/components/icons/BankIcon'; +import { createSenderReceiverHandler } from '../createSenderReceiverHandler'; +import { registerHandler } from '@/components/bank/handlers/handlerRegistry'; +import { MsgSend } from '@liftedinit/manifestjs/dist/codegen/cosmos/bank/v1beta1/tx'; +import { createTokenMessage } from '@/components'; + +export const MsgSendHandler = createSenderReceiverHandler({ + iconSender: BankIcon, + successSender: (tx, _, metadata) => { + return createTokenMessage( + 'You sent {0} to {1}', + tx.metadata?.amount?.[0]?.amount, + tx.metadata?.amount?.[0]?.denom, + tx.metadata?.toAddress, + 'red', + metadata + ); + }, + failSender: (tx, _, metadata) => { + return createTokenMessage( + 'You failed to send {0} to {1}', + tx.metadata?.amount?.[0]?.amount, + tx.metadata?.amount?.[0]?.denom, + tx.metadata?.toAddress, + 'red', + metadata + ); + }, + successReceiver: (tx, _, metadata) => { + return createTokenMessage( + 'You received {0} from {1}', + tx.metadata?.amount?.[0]?.amount, + tx.metadata?.amount?.[0]?.denom, + tx.sender, + 'green', + metadata + ); + }, +}); + +registerHandler(MsgSend.typeUrl, MsgSendHandler); diff --git a/components/bank/handlers/createMessageUtils.tsx b/components/bank/handlers/createMessageUtils.tsx new file mode 100644 index 00000000..625a414b --- /dev/null +++ b/components/bank/handlers/createMessageUtils.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { formatAmount, formatDenom, formatLargeNumber } from '@/utils'; +import { format } from 'react-string-format'; +import { TruncatedAddressWithCopy } from '@/components/react/addressCopy'; +import { MetadataSDKType } from '@liftedinit/manifestjs/dist/codegen/cosmos/bank/v1beta1/bank'; + +export const createTokenMessage = ( + template: string, + amount: string, + denom: string, + address: string, + color: string, + metadata?: MetadataSDKType[] +) => { + const formattedAmount = formatLargeNumber(formatAmount(amount, denom, metadata)); + const formattedDenom = formatDenom(denom); + // coloredAmount is {0} + const coloredAmount = ( + + {formattedAmount} {formattedDenom} + + ); + const message = format( + template, + coloredAmount, + address ? : 'an unknown address' + ); + return {message}; +}; + +export const createValidatorMessage = ( + template: string, + validatorAddress: string, + sender?: string +) => { + const message = format( + template, + validatorAddress ? ( + + ) : ( + 'unknown' + ), + sender ? : 'an unknown address' + ); + return {message}; +}; diff --git a/components/bank/handlers/createSenderReceiverHandler.tsx b/components/bank/handlers/createSenderReceiverHandler.tsx new file mode 100644 index 00000000..a5c5bd53 --- /dev/null +++ b/components/bank/handlers/createSenderReceiverHandler.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import { MetadataSDKType } from '@liftedinit/manifestjs/dist/codegen/cosmos/bank/v1beta1/bank'; +import { QuestionIcon } from '@/components/icons/QuestionIcon'; +import { TxMessage } from '../types'; + +export function createSenderReceiverHandler({ + iconSender, + iconReceiver, + successSender, + failSender, + successReceiver, + failReceiver, +}: { + iconSender: React.ComponentType; + iconReceiver?: React.ComponentType; + successSender: + | string + | ((tx: TxMessage, address: string, metadata?: MetadataSDKType[]) => React.ReactNode); + failSender: + | string + | ((tx: TxMessage, address: string, metadata?: MetadataSDKType[]) => React.ReactNode); + successReceiver: + | string + | ((tx: TxMessage, address: string, metadata?: MetadataSDKType[]) => React.ReactNode); + failReceiver?: string | ((tx: TxMessage, address: string) => React.ReactNode); +}) { + return (tx: TxMessage, address: string, metadata?: MetadataSDKType[]) => { + const isSender = tx.sender === address; + const hasError = !!tx.error; + + iconSender = iconSender ?? QuestionIcon; + iconReceiver = iconReceiver ?? iconSender ?? QuestionIcon; + + const resolveMessage = ( + msg: + | React.ReactNode + | ((tx: TxMessage, address: string, metadata?: MetadataSDKType[]) => React.ReactNode) + ) => (typeof msg === 'function' ? msg(tx, address, metadata) : msg); + + const successSenderMsg = resolveMessage(successSender); + const failSenderMsg = resolveMessage(failSender); + const successReceiverMsg = resolveMessage(successReceiver); + const failReceiverMsg = resolveMessage(failReceiver ?? 'Anomaly detected'); + + return { + icon: isSender ? iconSender : iconReceiver, + message: hasError + ? isSender + ? failSenderMsg + : failReceiverMsg + : isSender + ? successSenderMsg + : successReceiverMsg, + }; + }; +} diff --git a/components/bank/handlers/defaultHandler.tsx b/components/bank/handlers/defaultHandler.tsx new file mode 100644 index 00000000..339be9d7 --- /dev/null +++ b/components/bank/handlers/defaultHandler.tsx @@ -0,0 +1,9 @@ +import { createSenderReceiverHandler } from '@/components/bank/handlers/createSenderReceiverHandler'; +import { QuestionIcon } from '@/components/icons/QuestionIcon'; + +export const DefaultHandler = createSenderReceiverHandler({ + iconSender: QuestionIcon, + successSender: 'Unknown transaction type', + failSender: 'Unknown transaction type', + successReceiver: 'Unknown transaction type', +}); diff --git a/components/bank/handlers/group/index.ts b/components/bank/handlers/group/index.ts new file mode 100644 index 00000000..eb34735c --- /dev/null +++ b/components/bank/handlers/group/index.ts @@ -0,0 +1,14 @@ +export * from './msgExecHandler'; +export * from './msgCreateGroupWithPolicyHandler'; +export * from './msgSubmitProposalHandler'; +export * from './msgVoteHandler'; +export * from './msgWithdrawProposalHandler'; +export * from './msgUpdateGroupMetadataHandler'; +export * from './msgUpdateGroupPolicyMetadataHandler'; +export * from './msgUpdateGroupPolicyDecisionPolicyHandler'; +export * from './msgLeaveGroupHandler'; +export * from './msgUpdateGroupMembersHandler'; +export * from './msgCreateGroupHandler'; +export * from './msgCreateGroupPolicyHandler'; +export * from './msgUpdateGroupAdminHandler'; +export * from './msgUpdateGroupPolicyAdminHandler'; diff --git a/components/bank/handlers/group/metadata.ts b/components/bank/handlers/group/metadata.ts new file mode 100644 index 00000000..896a0b74 --- /dev/null +++ b/components/bank/handlers/group/metadata.ts @@ -0,0 +1,29 @@ +import { truncateString } from '@/utils'; +import { ThresholdDecisionPolicy } from '@liftedinit/manifestjs/dist/codegen/cosmos/group/v1/types'; +import { PercentageDecisionPolicy } from 'cosmjs-types/cosmos/group/v1/types'; + +export function getGroupTitle(metadata: string): string | undefined { + let title = ''; + + try { + const parsed = JSON.parse(metadata); + title = parsed.title || title; + } catch (e) {} + + if (title === '') { + return undefined; + } + + return truncateString(title, 24); +} + +export function getGroupPolicy(policyType: string): string { + switch (policyType) { + case ThresholdDecisionPolicy.typeUrl: + return 'threshold'; + case PercentageDecisionPolicy.typeUrl: + return 'percentage'; + default: + return 'unknown'; + } +} diff --git a/components/bank/handlers/group/msgCreateGroupHandler.tsx b/components/bank/handlers/group/msgCreateGroupHandler.tsx new file mode 100644 index 00000000..32f11518 --- /dev/null +++ b/components/bank/handlers/group/msgCreateGroupHandler.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { createSenderReceiverHandler } from '@/components/bank/handlers/createSenderReceiverHandler'; +import { GroupsIcon } from '@/components/icons/GroupsIcon'; +import { format } from 'react-string-format'; +import { MsgCreateGroup } from '@liftedinit/manifestjs/dist/codegen/cosmos/group/v1/tx'; +import { registerHandler } from '@/components/bank/handlers/handlerRegistry'; + +const createMessage = (template: string, numMembers: number) => { + const message = format(template, numMembers); + return {message}; +}; + +export const MsgCreateGroupHandler = createSenderReceiverHandler({ + iconSender: GroupsIcon, + successSender: tx => { + return createMessage('You created a group with {0} members', tx.metadata?.members?.length); + }, + failSender: tx => { + return createMessage( + 'You failed to create a group with {0} members', + tx.metadata?.members?.length + ); + }, + successReceiver: tx => { + return createMessage( + 'You were added to a group with {0} members', + tx.metadata?.members?.length + ); + }, +}); + +registerHandler(MsgCreateGroup.typeUrl, MsgCreateGroupHandler); diff --git a/components/bank/handlers/group/msgCreateGroupPolicyHandler.tsx b/components/bank/handlers/group/msgCreateGroupPolicyHandler.tsx new file mode 100644 index 00000000..9d73d712 --- /dev/null +++ b/components/bank/handlers/group/msgCreateGroupPolicyHandler.tsx @@ -0,0 +1,39 @@ +import { registerHandler } from '@/components/bank/handlers/handlerRegistry'; +import { GroupsIcon } from '@/components/icons/GroupsIcon'; +import { MsgCreateGroupPolicy } from '@liftedinit/manifestjs/dist/codegen/cosmos/group/v1/tx'; +import { createSenderReceiverHandler } from '@/components/bank/handlers/createSenderReceiverHandler'; +import { format } from 'react-string-format'; +import { getGroupPolicy } from '@/components/bank/handlers/group/metadata'; + +const createMessage = (template: string, policyType: string, groupId: string) => { + const policy = getGroupPolicy(policyType); + const message = format(template, policy, groupId); + return {message}; +}; + +export const MsgCreateGroupPolicyHandler = createSenderReceiverHandler({ + iconSender: GroupsIcon, + successSender: tx => { + return createMessage( + 'You created a {0} decision policy for group #{1}', + tx.metadata?.decisionPolicy?.['@type'], + tx.metadata?.groupId + ); + }, + failSender: tx => { + return createMessage( + 'You failed to create a {0} decision policy for group #{1}', + tx.metadata?.decisionPolicy?.['@type'], + tx.metadata?.groupId + ); + }, + successReceiver: tx => { + return createMessage( + 'A {0} decision policy was created for group #{1}', + tx.metadata?.decisionPolicy?.['@type'], + tx.metadata?.groupId + ); + }, +}); + +registerHandler(MsgCreateGroupPolicy.typeUrl, MsgCreateGroupPolicyHandler); diff --git a/components/bank/handlers/group/msgCreateGroupWithPolicyHandler.tsx b/components/bank/handlers/group/msgCreateGroupWithPolicyHandler.tsx new file mode 100644 index 00000000..b27c8edd --- /dev/null +++ b/components/bank/handlers/group/msgCreateGroupWithPolicyHandler.tsx @@ -0,0 +1,24 @@ +import { GroupsIcon } from '@/components/icons/GroupsIcon'; +import { createSenderReceiverHandler } from '../createSenderReceiverHandler'; +import { registerHandler } from '@/components/bank/handlers/handlerRegistry'; +import { MsgCreateGroupWithPolicy } from '@liftedinit/manifestjs/dist/codegen/cosmos/group/v1/tx'; +import { getGroupTitle } from '@/components/bank/handlers/group/metadata'; +import { format } from 'react-string-format'; + +const createMessage = (template: string, metadata: any) => { + const title = getGroupTitle(metadata); + const named = title ? `named: ${title}` : 'with an unknown name'; + const message = format(template, named); + return {message}; +}; + +export const MsgCreateGroupWithPolicyHandler = createSenderReceiverHandler({ + iconSender: GroupsIcon, + successSender: tx => createMessage('You created a new group {0}', tx.metadata?.groupMetadata), + failSender: tx => + createMessage('You failed to create a new group {0}', tx.metadata?.groupMetadata), + successReceiver: tx => + createMessage('You were mentioned in a new group {0}', tx.metadata?.groupMetadata), +}); + +registerHandler(MsgCreateGroupWithPolicy.typeUrl, MsgCreateGroupWithPolicyHandler); diff --git a/components/bank/handlers/group/msgExecHandler.tsx b/components/bank/handlers/group/msgExecHandler.tsx new file mode 100644 index 00000000..cd594539 --- /dev/null +++ b/components/bank/handlers/group/msgExecHandler.tsx @@ -0,0 +1,27 @@ +import { GroupsIcon } from '@/components/icons/GroupsIcon'; +import { MsgExec } from '@liftedinit/manifestjs/dist/codegen/cosmos/group/v1/tx'; +import { createSenderReceiverHandler } from '../createSenderReceiverHandler'; +import { registerHandler } from '../handlerRegistry'; +import { TruncatedAddressWithCopy } from '@/components/react/addressCopy'; +import { format } from 'react-string-format'; + +const createMessage = (template: string, id: string, sender?: string) => { + const message = format( + template, + id ?? 'unknown', + sender ? : 'an unknown address' + ); + return {message}; +}; + +export const MsgExecHandler = createSenderReceiverHandler({ + iconSender: GroupsIcon, + successSender: tx => createMessage('You executed proposal #{0}', tx.proposal_ids?.[0]), // TODO Link to proposal + failSender: tx => createMessage('You failed to execute proposal #{0}', tx.proposal_ids?.[0]), // TODO Link to proposal + successReceiver: tx => + createMessage('Proposal #{0} was executed by {1}', tx.proposal_ids?.[0], tx.sender), // TODO Link to proposal + failReceiver: tx => + createMessage('Proposal #{0} failed to be executed by {1}', tx.proposal_ids?.[0], tx.sender), // TODO Link to proposal +}); + +registerHandler(MsgExec.typeUrl, MsgExecHandler); diff --git a/components/bank/handlers/group/msgLeaveGroupHandler.tsx b/components/bank/handlers/group/msgLeaveGroupHandler.tsx new file mode 100644 index 00000000..10730f2d --- /dev/null +++ b/components/bank/handlers/group/msgLeaveGroupHandler.tsx @@ -0,0 +1,13 @@ +import { GroupsIcon } from '@/components/icons/GroupsIcon'; +import { createSenderReceiverHandler } from '../createSenderReceiverHandler'; +import { registerHandler } from '@/components/bank/handlers/handlerRegistry'; +import { MsgLeaveGroup } from '@liftedinit/manifestjs/dist/codegen/cosmos/group/v1/tx'; + +export const MsgLeaveGroupHandler = createSenderReceiverHandler({ + iconSender: GroupsIcon, + successSender: tx => `You left group #${tx.metadata?.groupId}`, + failSender: tx => `You failed to leave group #${tx.metadata?.groupId}`, + successReceiver: tx => `Group #${tx.metadata?.groupId} had a member leave`, +}); + +registerHandler(MsgLeaveGroup.typeUrl, MsgLeaveGroupHandler); diff --git a/components/bank/handlers/group/msgSubmitProposalHandler.tsx b/components/bank/handlers/group/msgSubmitProposalHandler.tsx new file mode 100644 index 00000000..2ccc2f63 --- /dev/null +++ b/components/bank/handlers/group/msgSubmitProposalHandler.tsx @@ -0,0 +1,22 @@ +import { GroupsIcon } from '@/components/icons/GroupsIcon'; +import { createSenderReceiverHandler } from '../createSenderReceiverHandler'; +import { registerHandler } from '@/components/bank/handlers/handlerRegistry'; +import { MsgSubmitProposal } from '@liftedinit/manifestjs/dist/codegen/cosmos/group/v1/tx'; +import { TruncatedAddressWithCopy } from '@/components/react/addressCopy'; +import { format } from 'react-string-format'; + +const createMessage = (template: string, ids: string, sender: string) => { + const message = format(template, ids, ); + return {message}; +}; + +export const MsgSubmitProposalHandler = createSenderReceiverHandler({ + iconSender: GroupsIcon, + successSender: tx => + createMessage('You submitted proposal #{0} to {1}', tx.proposal_ids?.[0], tx.sender), // TODO Link to proposal + failSender: 'You failed to submit a proposal', + successReceiver: tx => + createMessage('Proposal #{0} was submitted by {1}', tx.proposal_ids?.[0], tx.sender), // TODO Link to proposal +}); + +registerHandler(MsgSubmitProposal.typeUrl, MsgSubmitProposalHandler); diff --git a/components/bank/handlers/group/msgUpdateGroupAdminHandler.tsx b/components/bank/handlers/group/msgUpdateGroupAdminHandler.tsx new file mode 100644 index 00000000..814b17d1 --- /dev/null +++ b/components/bank/handlers/group/msgUpdateGroupAdminHandler.tsx @@ -0,0 +1,42 @@ +import { registerHandler } from '@/components/bank/handlers/handlerRegistry'; +import { MsgUpdateGroupAdmin } from '@liftedinit/manifestjs/dist/codegen/cosmos/group/v1/tx'; +import { format } from 'react-string-format'; +import { createSenderReceiverHandler } from '@/components/bank/handlers/createSenderReceiverHandler'; +import { GroupsIcon } from '@/components/icons/GroupsIcon'; +import { TruncatedAddressWithCopy } from '@/components/react/addressCopy'; + +const createMessage = (template: string, groupId: number, newAdmin: string) => { + const message = format( + template, + groupId, + newAdmin ? : 'an unknown address' + ); + return {message}; +}; + +export const MsgUpdateGroupAdminHandler = createSenderReceiverHandler({ + iconSender: GroupsIcon, + successSender: tx => { + return createMessage( + 'You updated the administrator of group #{0} to {1}', + tx.metadata?.groupId, + tx.metadata?.newAdmin + ); + }, + failSender: tx => { + return createMessage( + 'You failed to update the administrator of group #{0} to {1}', + tx.metadata?.groupId, + tx.metadata?.newAdmin + ); + }, + successReceiver: tx => { + return createMessage( + 'You were made administrator of group #{0}', + tx.metadata?.groupId, + tx.metadata?.newAdmin + ); + }, +}); + +registerHandler(MsgUpdateGroupAdmin.typeUrl, MsgUpdateGroupAdminHandler); diff --git a/components/bank/handlers/group/msgUpdateGroupMembersHandler.tsx b/components/bank/handlers/group/msgUpdateGroupMembersHandler.tsx new file mode 100644 index 00000000..ed71092f --- /dev/null +++ b/components/bank/handlers/group/msgUpdateGroupMembersHandler.tsx @@ -0,0 +1,19 @@ +import { GroupsIcon } from '@/components/icons/GroupsIcon'; +import { createSenderReceiverHandler } from '../createSenderReceiverHandler'; +import { registerHandler } from '@/components/bank/handlers/handlerRegistry'; +import { MsgUpdateGroupMembers } from '@liftedinit/manifestjs/dist/codegen/cosmos/group/v1/tx'; +import { format } from 'react-string-format'; + +const createMessage = (template: string, groupId: string) => { + const message = format(template, groupId); + return {message}; +}; + +export const MsgUpdateGroupMembersHandler = createSenderReceiverHandler({ + iconSender: GroupsIcon, + successSender: tx => createMessage('You updated the members of group #{0}', tx.metadata?.groupId), + failSender: 'You failed to update group members', + successReceiver: tx => createMessage('Group #{0} had its members updated', tx.metadata.groupId), +}); + +registerHandler(MsgUpdateGroupMembers.typeUrl, MsgUpdateGroupMembersHandler); diff --git a/components/bank/handlers/group/msgUpdateGroupMetadataHandler.tsx b/components/bank/handlers/group/msgUpdateGroupMetadataHandler.tsx new file mode 100644 index 00000000..275e2fc2 --- /dev/null +++ b/components/bank/handlers/group/msgUpdateGroupMetadataHandler.tsx @@ -0,0 +1,13 @@ +import { GroupsIcon } from '@/components/icons/GroupsIcon'; +import { createSenderReceiverHandler } from '../createSenderReceiverHandler'; +import { registerHandler } from '@/components/bank/handlers/handlerRegistry'; +import { MsgUpdateGroupMetadata } from '@liftedinit/manifestjs/dist/codegen/cosmos/group/v1/tx'; + +export const MsgUpdateGroupMetadataHandler = createSenderReceiverHandler({ + iconSender: GroupsIcon, + successSender: tx => <>You updated the metadata of group #{tx.metadata?.groupId}, + failSender: tx => <>You failed to update the metadata of group #{tx.metadata?.groupId}, + successReceiver: tx => <>Group #{tx.metadata?.groupId} had its metadata updated, +}); + +registerHandler(MsgUpdateGroupMetadata.typeUrl, MsgUpdateGroupMetadataHandler); diff --git a/components/bank/handlers/group/msgUpdateGroupPolicyAdminHandler.tsx b/components/bank/handlers/group/msgUpdateGroupPolicyAdminHandler.tsx new file mode 100644 index 00000000..38e80c9c --- /dev/null +++ b/components/bank/handlers/group/msgUpdateGroupPolicyAdminHandler.tsx @@ -0,0 +1,53 @@ +import { registerHandler } from '@/components/bank/handlers/handlerRegistry'; +import { GroupsIcon } from '@/components/icons/GroupsIcon'; +import { MsgUpdateGroupPolicyAdmin } from '@liftedinit/manifestjs/dist/codegen/cosmos/group/v1/tx'; +import { createSenderReceiverHandler } from '@/components/bank/handlers/createSenderReceiverHandler'; +import { format } from 'react-string-format'; +import { TruncatedAddressWithCopy } from '@/components/react/addressCopy'; + +const createMessage = ( + template: string, + groupPolicyAddr: string, + newAdmin: string, + sender?: string +) => { + const message = format( + template, + groupPolicyAddr ? ( + + ) : ( + 'an unknown address' + ), + newAdmin ? : 'an unknown address', + sender ? : 'an unknown address' + ); + return {message}; +}; + +export const MsgUpdateGroupPolicyAdminHandler = createSenderReceiverHandler({ + iconSender: GroupsIcon, + successSender: tx => { + return createMessage( + 'You updated the group policy {0} administrator to {1}', + tx.metadata?.groupPolicyAddress, + tx.metadata?.newAdmin + ); + }, + failSender: tx => { + return createMessage( + 'You failed to update the group policy {0} administrator to {1}', + tx.metadata?.groupPolicyAddress, + tx.metadata?.newAdmin + ); + }, + successReceiver: tx => { + return createMessage( + 'You were made administrator of group policy {0} by {2}', + tx.metadata?.groupPolicyAddress, + tx.metadata?.newAdmin, + tx.sender + ); + }, +}); + +registerHandler(MsgUpdateGroupPolicyAdmin.typeUrl, MsgUpdateGroupPolicyAdminHandler); diff --git a/components/bank/handlers/group/msgUpdateGroupPolicyDecisionPolicyHandler.tsx b/components/bank/handlers/group/msgUpdateGroupPolicyDecisionPolicyHandler.tsx new file mode 100644 index 00000000..ae5a9fa5 --- /dev/null +++ b/components/bank/handlers/group/msgUpdateGroupPolicyDecisionPolicyHandler.tsx @@ -0,0 +1,32 @@ +import { GroupsIcon } from '@/components/icons/GroupsIcon'; +import { createSenderReceiverHandler } from '../createSenderReceiverHandler'; +import { registerHandler } from '@/components/bank/handlers/handlerRegistry'; +import { MsgUpdateGroupPolicyDecisionPolicy } from '@liftedinit/manifestjs/dist/codegen/cosmos/group/v1/tx'; +import { TruncatedAddressWithCopy } from '@/components/react/addressCopy'; +import { format } from 'react-string-format'; + +const createMessage = (template: string, policyAddress: string) => { + const message = format( + template, + policyAddress ? : 'unknown' + ); + return {message}; +}; + +export const MsgUpdateGroupPolicyDecisionPolicyHandler = createSenderReceiverHandler({ + iconSender: GroupsIcon, + successSender: tx => + createMessage('You updated the decision policy of group {0}', tx.metadata?.groupPolicyAddress), + failSender: tx => + createMessage( + 'You failed to update the decision policy of group {0}', + tx.metadata?.groupPolicyAddress + ), + successReceiver: tx => + createMessage('Group {0} had its decision policy updated', tx.metadata.groupPolicyAddress), +}); + +registerHandler( + MsgUpdateGroupPolicyDecisionPolicy.typeUrl, + MsgUpdateGroupPolicyDecisionPolicyHandler +); diff --git a/components/bank/handlers/group/msgUpdateGroupPolicyMetadataHandler.tsx b/components/bank/handlers/group/msgUpdateGroupPolicyMetadataHandler.tsx new file mode 100644 index 00000000..9d3372fc --- /dev/null +++ b/components/bank/handlers/group/msgUpdateGroupPolicyMetadataHandler.tsx @@ -0,0 +1,27 @@ +import { GroupsIcon } from '@/components/icons/GroupsIcon'; +import { createSenderReceiverHandler } from '../createSenderReceiverHandler'; +import { registerHandler } from '@/components/bank/handlers/handlerRegistry'; +import { MsgUpdateGroupPolicyMetadata } from '@liftedinit/manifestjs/dist/codegen/cosmos/group/v1/tx'; +import { TruncatedAddressWithCopy } from '@/components/react/addressCopy'; + +const createMessage = (prefix: string, policyAddress: string, suffix?: string) => { + return ( + + {prefix}{' '} + {policyAddress ? : 'unknown'}{' '} + {suffix} + + ); +}; + +export const MsgUpdateGroupPolicyMetadataHandler = createSenderReceiverHandler({ + iconSender: GroupsIcon, + successSender: tx => + createMessage('You updated the policy metadata of group', tx.metadata?.groupPolicyAddress), + failSender: tx => + createMessage('You failed to update policy metadata of group', tx.metadata?.groupPolicyAddress), + successReceiver: tx => + createMessage('Group', tx.metadata.groupPolicyAddress, 'had its policy metadata updated'), +}); + +registerHandler(MsgUpdateGroupPolicyMetadata.typeUrl, MsgUpdateGroupPolicyMetadataHandler); diff --git a/components/bank/handlers/group/msgVoteHandler.tsx b/components/bank/handlers/group/msgVoteHandler.tsx new file mode 100644 index 00000000..9172465b --- /dev/null +++ b/components/bank/handlers/group/msgVoteHandler.tsx @@ -0,0 +1,38 @@ +import { GroupsIcon } from '@/components/icons/GroupsIcon'; +import { formatVote } from '@/utils'; +import { createSenderReceiverHandler } from '../createSenderReceiverHandler'; +import { registerHandler } from '@/components/bank/handlers/handlerRegistry'; +import { MsgVote } from '@liftedinit/manifestjs/dist/codegen/cosmos/group/v1/tx'; +import { TruncatedAddressWithCopy } from '@/components/react/addressCopy'; +import { format } from 'react-string-format'; + +const createMessage = (template: string, vote: string, ids: string, sender?: string) => { + const message = format( + template, + formatVote(vote), + ids, + sender ? : 'an unknown address' + ); + return {message}; +}; + +export const MsgVoteHandler = createSenderReceiverHandler({ + iconSender: GroupsIcon, + successSender: tx => + createMessage('You voted {0} on proposal #{1}', tx.metadata?.option, tx.proposal_ids?.[0]), // TODO Link to proposal + failSender: tx => + createMessage( + 'You failed to vote {0} on proposal #{1}', + tx.metadata?.option, + tx.proposal_ids?.[0] + ), // TODO Link to proposal + successReceiver: tx => + createMessage( + 'Proposal #{1} was voted on {0} by {2}', + tx.metadata?.option, + tx.proposal_ids?.[0], + tx.sender + ), // TODO Link to proposal +}); + +registerHandler(MsgVote.typeUrl, MsgVoteHandler); diff --git a/components/bank/handlers/group/msgWithdrawProposalHandler.tsx b/components/bank/handlers/group/msgWithdrawProposalHandler.tsx new file mode 100644 index 00000000..6e0ae86d --- /dev/null +++ b/components/bank/handlers/group/msgWithdrawProposalHandler.tsx @@ -0,0 +1,25 @@ +import { GroupsIcon } from '@/components/icons/GroupsIcon'; +import { createSenderReceiverHandler } from '../createSenderReceiverHandler'; +import { registerHandler } from '@/components/bank/handlers/handlerRegistry'; +import { MsgWithdrawProposal } from '@liftedinit/manifestjs/dist/codegen/cosmos/group/v1/tx'; +import { TruncatedAddressWithCopy } from '@/components/react/addressCopy'; +import { format } from 'react-string-format'; + +const createMessage = (template: string, ids: string, sender?: string) => { + const message = format( + template, + ids, + sender ? : 'an unknown address' + ); + return {message}; +}; + +export const MsgWithdrawProposalHandler = createSenderReceiverHandler({ + iconSender: GroupsIcon, + successSender: tx => createMessage('You withdrew proposal #{0}', tx.proposal_ids?.[0]), // TODO Link to proposal + failSender: tx => createMessage('You failed to withdraw proposal #{0}', tx.proposal_ids?.[0]), // TODO Link to proposal + successReceiver: tx => + createMessage('Proposal #{0} was withdrawn by {1}', tx.proposal_ids?.[0], tx.sender), // TODO Link to proposal +}); + +registerHandler(MsgWithdrawProposal.typeUrl, MsgWithdrawProposalHandler); diff --git a/components/bank/handlers/handlerRegistry.ts b/components/bank/handlers/handlerRegistry.ts new file mode 100644 index 00000000..51f0abdb --- /dev/null +++ b/components/bank/handlers/handlerRegistry.ts @@ -0,0 +1,24 @@ +import React from 'react'; +import { QuestionIcon } from '@/components/icons'; +import { MetadataSDKType } from '@liftedinit/manifestjs/dist/codegen/cosmos/bank/v1beta1/bank'; +import { TxMessage } from '../types'; +import { DefaultHandler } from '@/components/bank/handlers/defaultHandler'; + +export type Handler = ( + tx: TxMessage, + address: string, + metadata?: MetadataSDKType[] +) => { + icon: React.ComponentType; + message: React.ReactNode; +}; + +const handlerRegistry: { [key: string]: Handler } = {}; + +export function registerHandler(typeUrl: string, handler: Handler) { + handlerRegistry[typeUrl] = handler; +} + +export function getHandler(typeUrl: string): Handler { + return handlerRegistry[typeUrl] || DefaultHandler; +} diff --git a/components/bank/handlers/ibc/index.ts b/components/bank/handlers/ibc/index.ts new file mode 100644 index 00000000..25511874 --- /dev/null +++ b/components/bank/handlers/ibc/index.ts @@ -0,0 +1 @@ +export * from './msgTransferHandler'; diff --git a/components/bank/handlers/ibc/msgTransferHandler.tsx b/components/bank/handlers/ibc/msgTransferHandler.tsx new file mode 100644 index 00000000..5cf46d06 --- /dev/null +++ b/components/bank/handlers/ibc/msgTransferHandler.tsx @@ -0,0 +1,38 @@ +import { TransferIcon } from '@/components/icons/TransferIcon'; +import { createSenderReceiverHandler } from '../createSenderReceiverHandler'; +import { registerHandler } from '@/components/bank/handlers/handlerRegistry'; +import { MsgTransfer } from '@liftedinit/manifestjs/dist/codegen/ibc/applications/transfer/v1/tx'; +import { createTokenMessage } from '@/components'; + +export const MsgTransferHandler = createSenderReceiverHandler({ + iconSender: TransferIcon, + successSender: (tx, _, metadata) => + createTokenMessage( + 'You sent {0} to {1} via IBC', + tx.metadata?.token?.amount, + tx.metadata?.token?.denom, + tx.metadata?.receiver, + 'red', + metadata + ), + failSender: (tx, _, metadata) => + createTokenMessage( + 'You failed to send {0} to {1} via IBC', + tx.metadata?.token?.amount, + tx.metadata?.token?.denom, + tx.metadata?.receiver, + 'red', + metadata + ), + successReceiver: (tx, _, metadata) => + createTokenMessage( + 'You received {0} from {1} via IBC', + tx.metadata?.token?.amount, + tx.metadata?.token?.denom, + tx.sender, + 'green', + metadata + ), +}); + +registerHandler(MsgTransfer.typeUrl, MsgTransferHandler); diff --git a/components/bank/handlers/index.ts b/components/bank/handlers/index.ts new file mode 100644 index 00000000..c14d6474 --- /dev/null +++ b/components/bank/handlers/index.ts @@ -0,0 +1,9 @@ +export * from './bank'; +export * from './ibc'; +export * from './tokenfactory'; +export * from './manifest'; +export * from './group'; +export * from './upgrade'; +export * from './poa'; +export { createTokenMessage } from '@/components/bank/handlers/createMessageUtils'; +export { createValidatorMessage } from '@/components/bank/handlers/createMessageUtils'; diff --git a/components/bank/handlers/manifest/index.ts b/components/bank/handlers/manifest/index.ts new file mode 100644 index 00000000..10f49cde --- /dev/null +++ b/components/bank/handlers/manifest/index.ts @@ -0,0 +1,2 @@ +export * from './msgPayoutHandler'; +export * from './msgBurnHeldBalanceHandler'; diff --git a/components/bank/handlers/manifest/msgBurnHeldBalanceHandler.tsx b/components/bank/handlers/manifest/msgBurnHeldBalanceHandler.tsx new file mode 100644 index 00000000..23dfd3c9 --- /dev/null +++ b/components/bank/handlers/manifest/msgBurnHeldBalanceHandler.tsx @@ -0,0 +1,30 @@ +import { BurnIcon } from '@/components/icons/BurnIcon'; +import { createSenderReceiverHandler } from '../createSenderReceiverHandler'; +import { registerHandler } from '@/components/bank/handlers/handlerRegistry'; +import { MsgBurnHeldBalance } from '@liftedinit/manifestjs/dist/codegen/liftedinit/manifest/v1/tx'; +import { createTokenMessage } from '@/components'; + +export const MsgBurnHeldBalanceHandler = createSenderReceiverHandler({ + iconSender: BurnIcon, + successSender: (tx, _, metadata) => + createTokenMessage( + 'You burned {0} from {1}', + tx.metadata?.burnCoins?.[0]?.amount, + tx.metadata?.burnCoins?.[0]?.denom, + tx.sender, + 'red', + metadata + ), + failSender: 'You failed to burn tokens', + successReceiver: (tx, _, metadata) => + createTokenMessage( + 'You were burned {0} by {1}', + tx.metadata?.burnCoins?.[0]?.amount, + tx.metadata?.burnCoins?.[0]?.denom, + tx.sender, + 'red', + metadata + ), +}); + +registerHandler(MsgBurnHeldBalance.typeUrl, MsgBurnHeldBalanceHandler); diff --git a/components/bank/handlers/manifest/msgPayoutHandler.tsx b/components/bank/handlers/manifest/msgPayoutHandler.tsx new file mode 100644 index 00000000..8997a56c --- /dev/null +++ b/components/bank/handlers/manifest/msgPayoutHandler.tsx @@ -0,0 +1,45 @@ +import { MintIcon } from '@/components/icons/MintIcon'; +import { createSenderReceiverHandler } from '../createSenderReceiverHandler'; +import { registerHandler } from '@/components/bank/handlers/handlerRegistry'; +import { MsgPayout } from '@liftedinit/manifestjs/dist/codegen/liftedinit/manifest/v1/tx'; +import { TruncatedAddressWithCopy } from '@/components/react/addressCopy'; +import { createTokenMessage } from '@/components'; +import { format } from 'react-string-format'; + +export const MsgPayoutHandler = createSenderReceiverHandler({ + iconSender: MintIcon, + successSender: (tx, _, metadata) => { + return tx.metadata?.payoutPairs?.length > 1 + ? format('You minted tokens to {0} addresses', tx.metadata?.payoutPairs?.length) + : createTokenMessage( + 'You minted {0} to {1}', + tx.metadata?.payoutPairs?.[0]?.coin?.amount, + tx.metadata?.payoutPairs?.[0]?.coin?.denom, + tx.metadata?.payoutPairs?.[0]?.address, + 'green', + metadata + ); + }, + failSender: 'You failed to mint tokens', + successReceiver: (tx, _, metadata) => { + return tx.metadata?.payoutPairs?.length > 1 + ? format( + 'You were minted tokens by {0}', + tx.sender ? ( + + ) : ( + 'an unknown address' + ) + ) + : createTokenMessage( + 'You were minted {0} by {1}', + tx.metadata?.payoutPairs?.[0]?.coin?.amount, + tx.metadata?.payoutPairs?.[0]?.coin?.denom, + tx.sender, + 'green', + metadata + ); + }, +}); + +registerHandler(MsgPayout.typeUrl, MsgPayoutHandler); diff --git a/components/bank/handlers/poa/index.ts b/components/bank/handlers/poa/index.ts new file mode 100644 index 00000000..48428433 --- /dev/null +++ b/components/bank/handlers/poa/index.ts @@ -0,0 +1,4 @@ +export * from './msgSetPowerHandler'; +export * from './msgCreateValidatorHandler'; +export * from './msgRemovePendingValidatorHandler'; +export * from './msgRemoveValidatorHandler'; diff --git a/components/bank/handlers/poa/msgCreateValidatorHandler.tsx b/components/bank/handlers/poa/msgCreateValidatorHandler.tsx new file mode 100644 index 00000000..919974af --- /dev/null +++ b/components/bank/handlers/poa/msgCreateValidatorHandler.tsx @@ -0,0 +1,21 @@ +import { AdminsIcon } from '@/components/icons/AdminsIcon'; +import { createSenderReceiverHandler } from '../createSenderReceiverHandler'; +import { registerHandler } from '@/components/bank/handlers/handlerRegistry'; +import { MsgCreateValidator } from '@liftedinit/manifestjs/dist/codegen/strangelove_ventures/poa/v1/tx'; +import { createValidatorMessage } from '@/components'; + +export const MsgCreateValidatorHandler = createSenderReceiverHandler({ + iconSender: AdminsIcon, + successSender: tx => + createValidatorMessage('You created validator {0}', tx.metadata?.validatorAddress), + failSender: tx => + createValidatorMessage('You failed to create validator {0}', tx.metadata?.validatorAddress), + successReceiver: tx => + createValidatorMessage( + 'Validator {0} was created by {1}', + tx.metadata.validatorAddress, + tx.sender + ), +}); + +registerHandler(MsgCreateValidator.typeUrl, MsgCreateValidatorHandler); diff --git a/components/bank/handlers/poa/msgRemovePendingValidatorHandler.tsx b/components/bank/handlers/poa/msgRemovePendingValidatorHandler.tsx new file mode 100644 index 00000000..ad0cde94 --- /dev/null +++ b/components/bank/handlers/poa/msgRemovePendingValidatorHandler.tsx @@ -0,0 +1,24 @@ +import { AdminsIcon } from '@/components/icons/AdminsIcon'; +import { createSenderReceiverHandler } from '../createSenderReceiverHandler'; +import { registerHandler } from '@/components/bank/handlers/handlerRegistry'; +import { MsgRemovePending } from '@liftedinit/manifestjs/dist/codegen/strangelove_ventures/poa/v1/tx'; +import { createValidatorMessage } from '@/components'; + +export const MsgRemovePendingValidatorHandler = createSenderReceiverHandler({ + iconSender: AdminsIcon, + successSender: tx => + createValidatorMessage('You removed pending validator {0}', tx.metadata?.validatorAddress), + failSender: tx => + createValidatorMessage( + 'You failed to remove pending validator {0}', + tx.metadata?.validatorAddress + ), + successReceiver: tx => + createValidatorMessage( + 'Validator {0} was removed from pending by {1}', + tx.metadata?.validatorAddress, + tx.sender + ), +}); + +registerHandler(MsgRemovePending.typeUrl, MsgRemovePendingValidatorHandler); diff --git a/components/bank/handlers/poa/msgRemoveValidatorHandler.tsx b/components/bank/handlers/poa/msgRemoveValidatorHandler.tsx new file mode 100644 index 00000000..47f2769c --- /dev/null +++ b/components/bank/handlers/poa/msgRemoveValidatorHandler.tsx @@ -0,0 +1,21 @@ +import { AdminsIcon } from '@/components/icons/AdminsIcon'; +import { createSenderReceiverHandler } from '../createSenderReceiverHandler'; +import { registerHandler } from '@/components/bank/handlers/handlerRegistry'; +import { MsgRemoveValidator } from '@liftedinit/manifestjs/dist/codegen/strangelove_ventures/poa/v1/tx'; +import { createValidatorMessage } from '@/components'; + +export const MsgRemoveValidatorHandler = createSenderReceiverHandler({ + iconSender: AdminsIcon, + successSender: tx => + createValidatorMessage('You removed validator {0}', tx.metadata?.validatorAddress), + failSender: tx => + createValidatorMessage('You failed to remove validator {0}', tx.metadata?.validatorAddress), + successReceiver: tx => + createValidatorMessage( + 'Validator {0} was removed by {1}', + tx.metadata?.validatorAddress, + tx.sender + ), +}); + +registerHandler(MsgRemoveValidator.typeUrl, MsgRemoveValidatorHandler); diff --git a/components/bank/handlers/poa/msgSetPowerHandler.tsx b/components/bank/handlers/poa/msgSetPowerHandler.tsx new file mode 100644 index 00000000..4513d4eb --- /dev/null +++ b/components/bank/handlers/poa/msgSetPowerHandler.tsx @@ -0,0 +1,42 @@ +import { AdminsIcon } from '@/components/icons/AdminsIcon'; +import { createSenderReceiverHandler } from '../createSenderReceiverHandler'; +import { registerHandler } from '@/components/bank/handlers/handlerRegistry'; +import { MsgSetPower } from '@liftedinit/manifestjs/dist/codegen/strangelove_ventures/poa/v1/tx'; +import { TruncatedAddressWithCopy } from '@/components/react/addressCopy'; +import { format } from 'react-string-format'; + +const createMessage = (template: string, validatorAddress: string, power: number) => { + const message = format( + template, + validatorAddress ? ( + + ) : ( + 'unknown' + ), + power + ); + return {message}; +}; +export const MsgSetPowerHandler = createSenderReceiverHandler({ + iconSender: AdminsIcon, + successSender: tx => + createMessage( + 'You set the validator {0} power to {1}', + tx.metadata?.validatorAddress, + tx.metadata?.power + ), + failSender: tx => + createMessage( + 'You failed to set the validator {0} power to {1}', + tx.metadata?.validatorAddress, + tx.metadata?.power + ), + successReceiver: tx => + createMessage( + 'Validator {0} had its power set to {1}', + tx.metadata?.validatorAddress, + tx.metadata?.power + ), +}); + +registerHandler(MsgSetPower.typeUrl, MsgSetPowerHandler); diff --git a/components/bank/handlers/tokenfactory/index.ts b/components/bank/handlers/tokenfactory/index.ts new file mode 100644 index 00000000..5397f761 --- /dev/null +++ b/components/bank/handlers/tokenfactory/index.ts @@ -0,0 +1,5 @@ +export * from './msgMintHandler'; +export * from './msgBurnHandler'; +export * from './msgChangeAdminHandler'; +export * from './msgCreateDenomHandler'; +export * from './msgSetDenomMetadataHandler'; diff --git a/components/bank/handlers/tokenfactory/msgBurnHandler.tsx b/components/bank/handlers/tokenfactory/msgBurnHandler.tsx new file mode 100644 index 00000000..d6b255bd --- /dev/null +++ b/components/bank/handlers/tokenfactory/msgBurnHandler.tsx @@ -0,0 +1,38 @@ +import { FactoryIcon } from '@/components/icons/FactoryIcon'; +import { createSenderReceiverHandler } from '../createSenderReceiverHandler'; +import { registerHandler } from '@/components/bank/handlers/handlerRegistry'; +import { MsgBurn } from '@liftedinit/manifestjs/dist/codegen/osmosis/tokenfactory/v1beta1/tx'; +import { createTokenMessage } from '@/components'; + +export const MsgBurnHandler = createSenderReceiverHandler({ + iconSender: FactoryIcon, + successSender: (tx, _, metadata) => + createTokenMessage( + 'You burned {0} from {1}', + tx.metadata?.amount?.amount, + tx.metadata?.amount?.denom, + tx.metadata?.burnFromAddress, + 'red', + metadata + ), + failSender: (tx, _, metadata) => + createTokenMessage( + 'You failed to burn {0} from {1}', + tx.metadata?.amount?.amount, + tx.metadata?.amount?.denom, + tx.metadata?.burnFromAddress, + 'red', + metadata + ), + successReceiver: (tx, _, metadata) => + createTokenMessage( + 'You were burned {0} by {1}', + tx.metadata?.amount?.amount, + tx.metadata?.amount?.denom, + tx.sender, + 'red', + metadata + ), +}); + +registerHandler(MsgBurn.typeUrl, MsgBurnHandler); diff --git a/components/bank/handlers/tokenfactory/msgChangeAdminHandler.tsx b/components/bank/handlers/tokenfactory/msgChangeAdminHandler.tsx new file mode 100644 index 00000000..4144e180 --- /dev/null +++ b/components/bank/handlers/tokenfactory/msgChangeAdminHandler.tsx @@ -0,0 +1,40 @@ +import { TransferIcon } from '@/components/icons/TransferIcon'; +import { createSenderReceiverHandler } from '../createSenderReceiverHandler'; +import { formatDenom } from '@/utils'; +import { registerHandler } from '@/components/bank/handlers/handlerRegistry'; +import { MsgChangeAdmin } from '@liftedinit/manifestjs/dist/codegen/osmosis/tokenfactory/v1beta1/tx'; +import { TruncatedAddressWithCopy } from '@/components/react/addressCopy'; +import { format } from 'react-string-format'; + +const createMessage = (template: string, newAdmin: string, denom: string) => { + const message = format( + template, + newAdmin ? : 'unknown', + denom ? formatDenom(denom) : 'unknown' + ); + return {message}; +}; + +export const MsgChangeAdminHandler = createSenderReceiverHandler({ + iconSender: TransferIcon, + successSender: tx => + createMessage( + 'You changed the administrator of the {1} token to {0}', + tx.metadata?.newAdmin, + tx.metadata?.denom + ), + failSender: tx => + createMessage( + 'You failed to change the administrator of the {1} token to {0}', + tx.metadata?.newAdmin, + tx.metadata?.denom + ), + successReceiver: tx => + createMessage( + 'The administrator of the {1} token was changed to {0}', + tx.metadata?.newAdmin, + tx.metadata?.denom + ), +}); + +registerHandler(MsgChangeAdmin.typeUrl, MsgChangeAdminHandler); diff --git a/components/bank/handlers/tokenfactory/msgCreateDenomHandler.tsx b/components/bank/handlers/tokenfactory/msgCreateDenomHandler.tsx new file mode 100644 index 00000000..deaa87cc --- /dev/null +++ b/components/bank/handlers/tokenfactory/msgCreateDenomHandler.tsx @@ -0,0 +1,27 @@ +import { FactoryIcon } from '@/components/icons/FactoryIcon'; +import { createSenderReceiverHandler } from '../createSenderReceiverHandler'; +import { formatDenom } from '@/utils'; +import { registerHandler } from '@/components/bank/handlers/handlerRegistry'; +import { MsgCreateDenom } from '@liftedinit/manifestjs/dist/codegen/osmosis/tokenfactory/v1beta1/tx'; +import { format } from 'react-string-format'; +import { TruncatedAddressWithCopy } from '@/components/react/addressCopy'; + +const createMessage = (template: string, sender: string, subdenom: string) => { + const message = format( + template, + formatDenom(`factory/${sender}/${subdenom}`), + sender ? : 'an unknown address' + ); + return {message}; +}; +export const MsgCreateDenomHandler = createSenderReceiverHandler({ + iconSender: FactoryIcon, + successSender: tx => + createMessage('You created the {0} denomination', tx.sender, tx.metadata?.subdenom), + failSender: tx => + createMessage('You failed to create the {0} denomination', tx.sender, tx.metadata?.subdenom), + successReceiver: tx => + createMessage('The {0} denomination was created by {1}', tx.sender, tx.metadata?.subdenom), +}); + +registerHandler(MsgCreateDenom.typeUrl, MsgCreateDenomHandler); diff --git a/components/bank/handlers/tokenfactory/msgMintHandler.tsx b/components/bank/handlers/tokenfactory/msgMintHandler.tsx new file mode 100644 index 00000000..11687657 --- /dev/null +++ b/components/bank/handlers/tokenfactory/msgMintHandler.tsx @@ -0,0 +1,40 @@ +import { FactoryIcon } from '@/components/icons/FactoryIcon'; +import { formatAmount, formatDenom, formatLargeNumber } from '@/utils'; +import { createSenderReceiverHandler } from '../createSenderReceiverHandler'; +import { registerHandler } from '@/components/bank/handlers/handlerRegistry'; +import { MsgMint } from '@liftedinit/manifestjs/dist/codegen/osmosis/tokenfactory/v1beta1/tx'; +import { TruncatedAddressWithCopy } from '@/components/react/addressCopy'; +import { createTokenMessage } from '@/components'; + +export const MsgMintHandler = createSenderReceiverHandler({ + iconSender: FactoryIcon, + successSender: (tx, _, metadata) => + createTokenMessage( + 'You minted {0} to {1}', + tx.metadata?.amount?.amount, + tx.metadata?.amount?.denom, + tx.metadata?.mintToAddress, + 'green', + metadata + ), + failSender: (tx, _, metadata) => + createTokenMessage( + 'You failed to mint {0} to {1}', + tx.metadata?.amount?.amount, + tx.metadata?.amount?.denom, + tx.metadata?.mintToAddress, + 'red', + metadata + ), + successReceiver: (tx, _, metadata) => + createTokenMessage( + 'You were minted {0} from {1}', + tx.metadata?.amount?.amount, + tx.metadata?.amount?.denom, + tx.sender, + 'green', + metadata + ), +}); + +registerHandler(MsgMint.typeUrl, MsgMintHandler); diff --git a/components/bank/handlers/tokenfactory/msgSetDenomMetadataHandler.tsx b/components/bank/handlers/tokenfactory/msgSetDenomMetadataHandler.tsx new file mode 100644 index 00000000..332215c8 --- /dev/null +++ b/components/bank/handlers/tokenfactory/msgSetDenomMetadataHandler.tsx @@ -0,0 +1,21 @@ +import { FactoryIcon } from '@/components/icons/FactoryIcon'; +import { createSenderReceiverHandler } from '../createSenderReceiverHandler'; +import { formatDenom } from '@/utils'; +import { registerHandler } from '@/components/bank/handlers/handlerRegistry'; +import { MsgSetDenomMetadata } from '@liftedinit/manifestjs/dist/codegen/osmosis/tokenfactory/v1beta1/tx'; +import { format } from 'react-string-format'; + +const createMessage = (template: string, base: string) => { + const message = format(template, formatDenom(base)); + return {message}; +}; +export const MsgSetDenomMetadataHandler = createSenderReceiverHandler({ + iconSender: FactoryIcon, + successSender: tx => createMessage('You set the metadata of denomination {0}', tx.metadata?.base), + failSender: tx => + createMessage('You failed to set the metadata of denomination {0}', tx.metadata?.base), + successReceiver: tx => + createMessage('The {0} denomination had its metadata set', tx.metadata?.base), +}); + +registerHandler(MsgSetDenomMetadata.typeUrl, MsgSetDenomMetadataHandler); diff --git a/components/bank/handlers/upgrade/index.ts b/components/bank/handlers/upgrade/index.ts new file mode 100644 index 00000000..055dff99 --- /dev/null +++ b/components/bank/handlers/upgrade/index.ts @@ -0,0 +1,2 @@ +export * from './msgSoftwareUpgradeHandler'; +export * from './msgCancelUpgradeHandler'; diff --git a/components/bank/handlers/upgrade/msgCancelUpgradeHandler.tsx b/components/bank/handlers/upgrade/msgCancelUpgradeHandler.tsx new file mode 100644 index 00000000..9dd1d956 --- /dev/null +++ b/components/bank/handlers/upgrade/msgCancelUpgradeHandler.tsx @@ -0,0 +1,24 @@ +import { ArrowUpIcon } from '@/components/icons/ArrowUpIcon'; +import { createSenderReceiverHandler } from '../createSenderReceiverHandler'; +import { registerHandler } from '@/components/bank/handlers/handlerRegistry'; +import { MsgCancelUpgrade } from '@liftedinit/manifestjs/dist/codegen/cosmos/upgrade/v1beta1/tx'; +import { TruncatedAddressWithCopy } from '@/components/react/addressCopy'; +import { format } from 'react-string-format'; + +const createMessage = (template: string, sender: string) => { + const message = format( + template, + sender ? : 'an unknown address' + ); + return {message}; +}; + +export const MsgCancelUpgradeHandler = createSenderReceiverHandler({ + iconSender: ArrowUpIcon, + successSender: 'You successfully cancelled the chain upgrade', + failSender: 'You failed to cancel chain software upgrade', + successReceiver: tx => + createMessage('The chain software upgrade was cancelled by {0}', tx.sender), +}); + +registerHandler(MsgCancelUpgrade.typeUrl, MsgCancelUpgradeHandler); diff --git a/components/bank/handlers/upgrade/msgSoftwareUpgradeHandler.tsx b/components/bank/handlers/upgrade/msgSoftwareUpgradeHandler.tsx new file mode 100644 index 00000000..4bc2d6ac --- /dev/null +++ b/components/bank/handlers/upgrade/msgSoftwareUpgradeHandler.tsx @@ -0,0 +1,41 @@ +import { ArrowUpIcon } from '@/components/icons/ArrowUpIcon'; +import { createSenderReceiverHandler } from '../createSenderReceiverHandler'; +import { registerHandler } from '@/components/bank/handlers/handlerRegistry'; +import { MsgSoftwareUpgrade } from '@liftedinit/manifestjs/dist/codegen/cosmos/upgrade/v1beta1/tx'; +import { format } from 'react-string-format'; +import { TruncatedAddressWithCopy } from '@/components/react/addressCopy'; + +const createMessage = (template: string, planName: string, planHeight: string, sender?: string) => { + const message = format( + template, + planName, + planHeight, + sender ? : 'an unknown address' + ); + return {message}; +}; + +export const MsgSoftwareUpgradeHandler = createSenderReceiverHandler({ + iconSender: ArrowUpIcon, + successSender: tx => + createMessage( + 'You scheduled a chain upgrade to {0} at block {1}', + tx.metadata?.plan?.name, + tx.metadata?.plan?.height + ), + failSender: tx => + createMessage( + 'You failed to schedule a chain software upgrade to {0} at block {1}', + tx.metadata?.plan?.name, + tx.metadata.plan?.height + ), + successReceiver: tx => + createMessage( + 'A chain upgrade to {0} is scheduled at block {1} by {2}', + tx.metadata?.plan?.name, + tx.metadata?.plan?.height, + tx.sender + ), +}); + +registerHandler(MsgSoftwareUpgrade.typeUrl, MsgSoftwareUpgradeHandler); diff --git a/components/bank/index.ts b/components/bank/index.ts index 83d6eb9c..28a431e4 100644 --- a/components/bank/index.ts +++ b/components/bank/index.ts @@ -1,3 +1,4 @@ export * from './forms'; export * from './components'; export * from './modals'; +export * from './handlers'; diff --git a/components/bank/modals/txInfo.tsx b/components/bank/modals/txInfo.tsx index c1f4547a..da75952f 100644 --- a/components/bank/modals/txInfo.tsx +++ b/components/bank/modals/txInfo.tsx @@ -1,17 +1,19 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { TruncatedAddressWithCopy } from '@/components/react/addressCopy'; -import { formatDenom, TransactionGroup } from '@/components'; +import { objectSyntax } from '@/components'; import { FaExternalLinkAlt } from 'react-icons/fa'; -import { denomToAsset, shiftDigits } from '@/utils'; import env from '@/config/env'; +import { useTheme } from '@/contexts'; +import { TxMessage } from '@/components/bank/types'; +import { isJsonString } from '@/utils/json'; interface TxInfoModalProps { - tx: TransactionGroup; - + tx: TxMessage; modalId: string; } export default function TxInfoModal({ tx, modalId }: TxInfoModalProps) { + const { theme } = useTheme(); function formatDate(dateString: string): string { const date = new Date(dateString); return date.toLocaleString('en-US', { @@ -24,6 +26,14 @@ export default function TxInfoModal({ tx, modalId }: TxInfoModalProps) { }); } + function isBase64(str: string): boolean { + try { + return btoa(atob(str)) === str; + } catch (err) { + return false; + } + } + return (
- - +
-
-

- VALUE -

-
- {tx?.data?.amount.map((amt, index) => { - const amount = Number(shiftDigits(amt.amount, -6)); - let displayDenom = formatDenom(amt.denom); - - if (amt.denom.startsWith('ibc/')) { - const assetInfo = denomToAsset(env.chain, amt.denom); - if (assetInfo?.traces?.[0]?.counterparty?.base_denom) { - displayDenom = assetInfo.traces[0].counterparty.base_denom.slice(1); - } - } - - return ( -

- {Number(amount).toLocaleString(undefined, { - maximumFractionDigits: 6, - })}{' '} - {displayDenom.toUpperCase()} -

- ); - })} -
-
+ {/*{tx?.metadata && (*/} + {/*
*/} + {/*

Metadata

*/} + {/* {Object.entries(tx?.metadata).map(([key, value], index) => (*/} + {/* */} + {/* ))}*/} + {/*
*/} + {/*)}*/} {tx.memo && (
)} + {tx.error && ( +
+ +
+ )}
@@ -112,17 +102,48 @@ export default function TxInfoModal({ tx, modalId }: TxInfoModalProps) { ); } +function MetadataItem({ + label, + content, + theme, +}: Readonly<{ + label: string; + content: any; + theme: string; +}>) { + const isJson = useMemo(() => isJsonString(content), [content]); + + return ( +
+

+ {label.toUpperCase().replace(/_/g, ' ')} +

+ {isJson ? ( + objectSyntax(JSON.parse(content), theme) + ) : typeof content === 'object' ? ( +
+
{JSON.stringify(content, null, 2)}
+
+ ) : ( +
+

{content}

+
+ )} +
+ ); +} + function InfoItem({ label, value, explorerUrl, isAddress = false, -}: { +}: Readonly<{ label: string; value: string; explorerUrl: string; isAddress?: boolean; -}) { +}>) { return (

{label}

diff --git a/components/bank/types.ts b/components/bank/types.ts new file mode 100644 index 00000000..c76b05bb --- /dev/null +++ b/components/bank/types.ts @@ -0,0 +1,24 @@ +export interface TransactionAmount { + amount: string; + denom: string; +} + +interface TxFee { + amount: TransactionAmount[]; + gas: string; +} + +export interface TxMessage { + id: string; + message_index: number; + type: string; + sender: string; + mentions: string[]; + metadata: any; + fee: TxFee; + memo: string; + height: number; + timestamp: string; + error: string; + proposal_ids: string[]; +} diff --git a/components/factory/components/DenomList.tsx b/components/factory/components/DenomList.tsx index d140c5d3..f2396b12 100644 --- a/components/factory/components/DenomList.tsx +++ b/components/factory/components/DenomList.tsx @@ -7,7 +7,7 @@ import { MintIcon, BurnIcon, TransferIcon } from '@/components/icons'; import { DenomInfoModal } from '@/components/factory/modals/denomInfo'; import MintModal from '@/components/factory/modals/MintModal'; import BurnModal from '@/components/factory/modals/BurnModal'; -import { UpdateDenomMetadataModal } from '@/components/factory/modals/updateDenomMetadata'; +import UpdateDenomMetadataModal from '@/components/factory/modals/updateDenomMetadata'; import { PiInfo } from 'react-icons/pi'; import useIsMobile from '@/hooks/useIsMobile'; import TransferModal from '@/components/factory/modals/TransferModal'; @@ -56,11 +56,35 @@ export default function DenomList({ currentPage * pageSize ); + const getBaseUrl = () => { + if (isGroup) { + return `/groups?policyAddress=${admin}&tab=tokens`; + } + return '/factory'; + }; + + const updateUrlWithModal = (action: string, denomBase?: string) => { + const baseUrl = getBaseUrl(); + const query: Record = isGroup ? { policyAddress: admin, tab: 'tokens' } : {}; + + if (action) query.action = action; + if (denomBase) query.denom = denomBase; + + router.push( + { + pathname: isGroup ? '/groups' : '/factory', + query, + }, + undefined, + { shallow: true } + ); + }; + const handleDenomSelect = (denom: ExtendedMetadataSDKType) => { if (!modalType) { setSelectedDenom(denom); setModalType('info'); - // router.push(`/factory?denom=${denom.base}&action=info`, undefined, { shallow: true }); + updateUrlWithModal('info', denom.base); } }; @@ -111,9 +135,7 @@ export default function DenomList({ setModalType(null); setOpenUpdateDenomMetadataModal(false); setOpenTransferDenomModal(false); - router.push(isGroup ? `/groups?policyAddress=${admin}&tab=tokens` : '/factory', undefined, { - shallow: true, - }); + updateUrlWithModal(''); }; const handleUpdateModalClose = () => { @@ -121,26 +143,13 @@ export default function DenomList({ setOpenUpdateDenomMetadataModal(false); setOpenTransferDenomModal(false); setModalType(null); - router.push(isGroup ? `/groups?policyAddress=${admin}&tab=tokens` : '/factory', undefined, { - shallow: true, - }); + updateUrlWithModal(''); }; - const handleUpdateModal = (denom: ExtendedMetadataSDKType, e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); + const handleUpdateModal = (denom: ExtendedMetadataSDKType) => { setSelectedDenom(denom); - setModalType('update'); setOpenUpdateDenomMetadataModal(true); - router.push( - isGroup - ? `/groups?policyAddress=${admin}&tab=tokens` - : `/factory?denom=${denom.base}&action=update`, - undefined, - { - shallow: true, - } - ); + updateUrlWithModal('update', denom.base); }; const handleTransferModal = (denom: ExtendedMetadataSDKType, e: React.MouseEvent) => { @@ -149,39 +158,41 @@ export default function DenomList({ setSelectedDenom(denom); setModalType('transfer'); setOpenTransferDenomModal(true); + updateUrlWithModal('transfer', denom.base); + }; + + const handleModalClose = () => { + setSelectedDenom(null); + setModalType(null); + // Remove modal type from URL router.push( - isGroup - ? `/groups?policyAddress=${admin}&tab=tokens` - : `/factory?denom=${denom.base}&action=transfer`, + { + pathname: isGroup ? '/groups' : '/factory', + query: isGroup ? { policyAddress: admin, tab: 'tokens' } : undefined, + }, undefined, { shallow: true } ); }; - const handleSwitchToMultiMint = () => { - setModalType('multimint'); - router.push( - isGroup - ? `/groups?policyAddress=${admin}&tab=tokens` - : `/factory?denom=${selectedDenom?.base}&action=multimint`, - undefined, - { - shallow: true, - } - ); + const handleMint = (denom: ExtendedMetadataSDKType, e: React.MouseEvent) => { + e.stopPropagation(); + setSelectedDenom(denom); + setModalType('mint'); + updateUrlWithModal('mint', denom.base); }; - const handleSwitchToMultiBurn = () => { - setModalType('multiburn'); - router.push( - isGroup - ? `/groups?policyAddress=${admin}&tab=tokens` - : `/factory?denom=${selectedDenom?.base}&action=multiburn`, - undefined, - { - shallow: true, - } - ); + const handleBurn = (denom: ExtendedMetadataSDKType, e: React.MouseEvent) => { + e.stopPropagation(); + setSelectedDenom(denom); + setModalType('burn'); + updateUrlWithModal('burn', denom.base); + }; + + const handleUpdate = (denom: ExtendedMetadataSDKType) => { + setSelectedDenom(denom); + setOpenUpdateDenomMetadataModal(true); + updateUrlWithModal('update', denom.base); }; return ( @@ -269,18 +280,10 @@ export default function DenomList({ key={denom.base} denom={denom} onSelectDenom={() => handleDenomSelect(denom)} - onMint={e => { - e.stopPropagation(); - setSelectedDenom(denom); - setModalType('mint'); - }} - onBurn={e => { - e.stopPropagation(); - setSelectedDenom(denom); - setModalType('burn'); - }} + onMint={e => handleMint(denom, e)} + onBurn={e => handleBurn(denom, e)} onTransfer={e => handleTransferModal(denom, e)} - onUpdate={e => handleUpdateModal(denom, e)} + onUpdate={() => handleUpdate(denom)} /> ))} @@ -401,7 +404,6 @@ export default function DenomList({ totalSupply={selectedDenom?.totalSupply ?? '0'} isOpen={modalType === 'mint'} onClose={handleCloseModal} - onSwitchToMultiMint={handleSwitchToMultiMint} isGroup={isGroup} /> { - refetch(); - handleUpdateModalClose(); - }} - openUpdateDenomMetadataModal={openUpdateDenomMetadataModal} - setOpenUpdateDenomMetadataModal={open => { - if (!open) { - handleUpdateModalClose(); - } else { - setOpenUpdateDenomMetadataModal(true); - } - }} + modalId="update-denom-metadata-modal" + onSuccess={refetchDenoms} admin={admin} isGroup={isGroup} /> { - if (!open) { - handleCloseModal(); - } else { - setOpenTransferDenomModal(true); - } - }} + isOpen={modalType === 'transfer'} + onClose={handleModalClose} onSuccess={() => { refetch(); - handleUpdateModalClose(); + handleModalClose(); }} - denom={selectedDenom} - address={address} - isOpen={modalType === 'transfer'} - onClose={handleCloseModal} - isGroup={isGroup} admin={admin} + isGroup={isGroup} />
); @@ -473,7 +457,7 @@ function TokenRow({ onMint: (e: React.MouseEvent) => void; onBurn: (e: React.MouseEvent) => void; onTransfer: (e: React.MouseEvent) => void; - onUpdate: (e: React.MouseEvent) => void; + onUpdate: () => void; }) { // Add safety checks for the values const exponent = denom?.denom_units?.[1]?.exponent ?? 0; @@ -542,11 +526,7 @@ function TokenRow({ diff --git a/components/factory/forms/BurnForm.tsx b/components/factory/forms/BurnForm.tsx index c9444a13..48290f51 100644 --- a/components/factory/forms/BurnForm.tsx +++ b/components/factory/forms/BurnForm.tsx @@ -28,7 +28,7 @@ interface BurnFormProps { refetch: () => void; balance: string; totalSupply: string; - onMultiBurnClick: () => void; + isGroup?: boolean; } @@ -40,13 +40,12 @@ export default function BurnForm({ refetch, balance, totalSupply, - onMultiBurnClick, + isGroup, }: Readonly) { const [amount, setAmount] = useState(''); - const [recipient, setRecipient] = useState(address); + const [recipient, setRecipient] = useState(address || ''); - const [isModalOpen, setIsModalOpen] = useState(false); const [burnPairs, setBurnPairs] = useState([{ address: '', amount: '' }]); const [isContactsOpen, setIsContactsOpen] = useState(false); @@ -83,21 +82,6 @@ export default function BurnForm({ recipient: Yup.string().required('Recipient address is required').manifestAddress(), }); - // Format balance safely - function formatAmount(amount: string | null | undefined): string { - if (amount == null) { - return '-'; - } - try { - return Number(shiftDigits(amount, -exponent)).toLocaleString(undefined, { - maximumFractionDigits: exponent, - }); - } catch (error) { - console.warn('Error formatting amount:', error); - return 'x'; - } - } - const handleBurn = async () => { if (!amount || Number.isNaN(Number(amount))) { return; @@ -212,7 +196,7 @@ export default function BurnForm({ fee, onSuccess: () => { setBurnPairs([{ address: '', amount: '' }]); - setIsModalOpen(false); + refetch(); }, }); @@ -283,10 +267,10 @@ export default function BurnForm({ label="AMOUNT" name="amount" placeholder="Enter amount" - value={amount} + value={amount || ''} onChange={(e: React.ChangeEvent) => { - setAmount(e.target.value); - setFieldValue('amount', e.target.value); + setAmount(e.target.value || ''); + setFieldValue('amount', e.target.value || ''); }} className={`input input-bordered w-full ${ touched.amount && errors.amount ? 'input-error' : '' @@ -307,10 +291,10 @@ export default function BurnForm({ label="TARGET" name="recipient" placeholder="Recipient address" - value={recipient} + value={recipient || ''} onChange={(e: React.ChangeEvent) => { - setRecipient(e.target.value); - setFieldValue('recipient', e.target.value); + setRecipient(e.target.value || ''); + setFieldValue('recipient', e.target.value || ''); }} className={`input input-bordered w-full transition-none ${ touched.recipient && errors.recipient ? 'input-error' : '' @@ -371,17 +355,6 @@ export default function BurnForm({ )}
- {isMFX && ( - - )}
); } diff --git a/components/factory/forms/MintForm.tsx b/components/factory/forms/MintForm.tsx index e71c20df..4ca5167c 100644 --- a/components/factory/forms/MintForm.tsx +++ b/components/factory/forms/MintForm.tsx @@ -22,7 +22,6 @@ export default function MintForm({ totalSupply, isGroup, admin, - onMultiMintClick, }: Readonly<{ isAdmin: boolean; denom: ExtendedMetadataSDKType; @@ -32,10 +31,9 @@ export default function MintForm({ totalSupply: string; isGroup?: boolean; admin?: string; - onMultiMintClick: () => void; }>) { const [amount, setAmount] = useState(''); - const [recipient, setRecipient] = useState(address); + const [recipient, setRecipient] = useState(address || ''); const [isContactsOpen, setIsContactsOpen] = useState(false); const { tx, isSigning, setIsSigning } = useTx(env.chain); @@ -160,10 +158,10 @@ export default function MintForm({ label="AMOUNT" name="amount" placeholder="Enter amount" - value={amount} + value={amount || ''} onChange={(e: React.ChangeEvent) => { - setAmount(e.target.value); - setFieldValue('amount', e.target.value); + setAmount(e.target.value || ''); + setFieldValue('amount', e.target.value || ''); }} className={`input input-bordered w-full ${ touched.amount && errors.amount ? 'input-error' : '' @@ -184,10 +182,10 @@ export default function MintForm({ label="RECIPIENT" name="recipient" placeholder="Recipient address" - value={recipient} + value={recipient || ''} onChange={(e: React.ChangeEvent) => { - setRecipient(e.target.value); - setFieldValue('recipient', e.target.value); + setRecipient(e.target.value || ''); + setFieldValue('recipient', e.target.value || ''); }} className={`input input-bordered w-full transition-none ${ touched.recipient && errors.recipient ? 'input-error' : '' @@ -250,17 +248,6 @@ export default function MintForm({ )}
- {isMFX && ( - - )} ); } diff --git a/components/factory/forms/__tests__/BurnForm.test.tsx b/components/factory/forms/__tests__/BurnForm.test.tsx index 0fc98294..4bebe85e 100644 --- a/components/factory/forms/__tests__/BurnForm.test.tsx +++ b/components/factory/forms/__tests__/BurnForm.test.tsx @@ -47,11 +47,6 @@ describe('BurnForm Component', () => { expect(screen.getByText('CIRCULATING SUPPLY')).toBeInTheDocument(); }); - test('renders multi burn when token is mfx', () => { - renderWithProps({ denom: mockMfxDenom }); - expect(screen.getByLabelText('multi-burn-button')).toBeInTheDocument(); - }); - test('renders not affiliated message when not admin and token is mfx', () => { renderWithProps({ isAdmin: false, denom: mockMfxDenom }); expect( diff --git a/components/factory/forms/__tests__/MintForm.test.tsx b/components/factory/forms/__tests__/MintForm.test.tsx index 51fe83f9..3dee46e9 100644 --- a/components/factory/forms/__tests__/MintForm.test.tsx +++ b/components/factory/forms/__tests__/MintForm.test.tsx @@ -86,9 +86,4 @@ describe('MintForm Component', () => { expect(mintButton).toBeEnabled(); }); }); - - test('renders multi mint button when token is mfx', () => { - renderWithProps({ denom: mockMfxDenom }); - expect(screen.getByLabelText('multi-mint-button')).toBeInTheDocument(); - }); }); diff --git a/components/factory/modals/BurnModal.tsx b/components/factory/modals/BurnModal.tsx index 438a0d7f..5b48e427 100644 --- a/components/factory/modals/BurnModal.tsx +++ b/components/factory/modals/BurnModal.tsx @@ -13,7 +13,7 @@ export default function BurnModal({ totalSupply, isOpen, onClose, - onSwitchToMultiBurn, + isGroup, }: { denom: ExtendedMetadataSDKType | null; @@ -24,7 +24,7 @@ export default function BurnModal({ totalSupply: string; isOpen: boolean; onClose: () => void; - onSwitchToMultiBurn: () => void; + isGroup?: boolean; }) { useEffect(() => { @@ -96,7 +96,6 @@ export default function BurnModal({ refetch={refetch} address={address} denom={denom} - onMultiBurnClick={onSwitchToMultiBurn} isGroup={isGroup} /> )} diff --git a/components/factory/modals/MintModal.tsx b/components/factory/modals/MintModal.tsx index cd40d704..0c67ffcf 100644 --- a/components/factory/modals/MintModal.tsx +++ b/components/factory/modals/MintModal.tsx @@ -14,7 +14,7 @@ export default function MintModal({ totalSupply, isOpen, onClose, - onSwitchToMultiMint, + admin, isGroup, }: { @@ -25,7 +25,7 @@ export default function MintModal({ totalSupply: string; isOpen: boolean; onClose: () => void; - onSwitchToMultiMint: () => void; + admin: string; isGroup?: boolean; }) { @@ -40,8 +40,6 @@ export default function MintModal({ return () => document.removeEventListener('keydown', handleEscape); }, [isOpen]); - const [isMultiMintOpen, setIsMultiMintOpen] = useState(false); - const { groupByAdmin, isGroupByAdminLoading } = useGroupsByAdmin(admin); if (!denom) return null; @@ -52,14 +50,6 @@ export default function MintModal({ const safeBalance = balance || '0'; const safeTotalSupply = totalSupply || '0'; - const handleMultiMintOpen = () => { - onSwitchToMultiMint(); - }; - - const handleMultiMintClose = () => { - setIsMultiMintOpen(false); - }; - const modalContent = ( )} diff --git a/components/factory/modals/TransferModal.tsx b/components/factory/modals/TransferModal.tsx index 1641796a..84249f15 100644 --- a/components/factory/modals/TransferModal.tsx +++ b/components/factory/modals/TransferModal.tsx @@ -16,8 +16,6 @@ const TokenOwnershipSchema = Yup.object().shape({ }); export default function TransferModal({ - openTransferDenomModal, - setOpenTransferDenomModal, denom, address, modalId, @@ -27,8 +25,6 @@ export default function TransferModal({ admin, isGroup, }: { - openTransferDenomModal: boolean; - setOpenTransferDenomModal: (open: boolean) => void; denom: ExtendedMetadataSDKType | null; address: string; modalId: string; @@ -51,14 +47,15 @@ export default function TransferModal({ const { setToastMessage } = useToast(); const handleCloseModal = (formikReset?: () => void) => { - setOpenTransferDenomModal(false); formikReset?.(); + onClose(); }; const { denomAuthority, isDenomAuthorityLoading } = useDenomAuthorityMetadata(denom?.base ?? ''); + const formData = { - denom: denom?.base ?? '', - currentAdmin: denomAuthority, + subdenom: denom?.base || '', + currentAdmin: denomAuthority?.admin || '', newAdmin: '', }; @@ -131,7 +128,7 @@ export default function TransferModal({ const modalContent = ( handleTransfer(values, resetForm)} validateOnChange={true} validateOnBlur={true} + enableReinitialize={true} > {({ isValid, dirty, values, handleChange, handleSubmit, resetForm }) => (
@@ -185,19 +183,26 @@ export default function TransferModal({ - +
@@ -95,11 +108,31 @@ export const DenomInfoModal: React.FC<{ />
- - + +
); + + // Only render if we're in the browser + if (typeof document !== 'undefined') { + return createPortal(modalContent, document.body); + } + + return null; }; function InfoItem({ diff --git a/components/factory/modals/updateDenomMetadata.tsx b/components/factory/modals/updateDenomMetadata.tsx index fd9b3604..e6f57546 100644 --- a/components/factory/modals/updateDenomMetadata.tsx +++ b/components/factory/modals/updateDenomMetadata.tsx @@ -11,6 +11,7 @@ import env from '@/config/env'; import { createPortal } from 'react-dom'; import { Any } from '@liftedinit/manifestjs/dist/codegen/google/protobuf/any'; import { MsgSetDenomMetadata } from '@liftedinit/manifestjs/dist/codegen/osmosis/tokenfactory/v1beta1/tx'; +import { useProposalsByPolicyAccount } from '@/hooks/useQueries'; const TokenDetailsSchema = (context: { subdenom: string }) => Yup.object().shape({ @@ -33,9 +34,9 @@ const TokenDetailsSchema = (context: { subdenom: string }) => .supportedImageUrl(), }); -export function UpdateDenomMetadataModal({ - openUpdateDenomMetadataModal, - setOpenUpdateDenomMetadataModal, +export default function UpdateDenomMetadataModal({ + isOpen, + onClose, denom, address, modalId, @@ -43,8 +44,8 @@ export function UpdateDenomMetadataModal({ admin, isGroup, }: { - openUpdateDenomMetadataModal: boolean; - setOpenUpdateDenomMetadataModal: (open: boolean) => void; + isOpen: boolean; + onClose: () => void; denom: ExtendedMetadataSDKType | null; address: string; modalId: string; @@ -52,21 +53,18 @@ export function UpdateDenomMetadataModal({ admin: string; isGroup?: boolean; }) { - const handleCloseModal = (formikReset?: () => void) => { - setOpenUpdateDenomMetadataModal(false); - formikReset?.(); - }; - + const { refetchProposals } = useProposalsByPolicyAccount(admin); useEffect(() => { const handleEscape = (e: KeyboardEvent) => { - if (e.key === 'Escape' && openUpdateDenomMetadataModal) { - handleCloseModal(); + if (e.key === 'Escape' && isOpen) { + onClose(); } }; document.addEventListener('keydown', handleEscape); return () => document.removeEventListener('keydown', handleEscape); - }, [openUpdateDenomMetadataModal]); + }, [isOpen, onClose]); + const baseDenom = denom?.base?.split('/').pop() || ''; const fullDenom = `factory/${address}/${baseDenom}`; const symbol = baseDenom.slice(1).toUpperCase(); @@ -82,9 +80,9 @@ export function UpdateDenomMetadataModal({ ], uri: denom?.uri || '', uriHash: denom?.uri_hash || '', - subdenom: baseDenom, + subdenom: baseDenom || '', exponent: '6', - label: fullDenom, + label: fullDenom || '', }; const { tx, isSigning, setIsSigning } = useTx(env.chain); const { estimateFee } = useFeeEstimation(env.chain); @@ -148,8 +146,11 @@ export function UpdateDenomMetadataModal({ await tx([msg], { fee, onSuccess: () => { + if (isGroup) { + refetchProposals(); + } onSuccess(); - handleCloseModal(resetForm); + onClose(); }, }); } catch (error) { @@ -162,7 +163,7 @@ export function UpdateDenomMetadataModal({ const modalContent = ( handleCloseModal(() => resetForm())} + onClick={() => onClose()} > ✕ @@ -253,7 +254,7 @@ export function UpdateDenomMetadataModal({ @@ -282,7 +283,7 @@ export function UpdateDenomMetadataModal({ backgroundColor: 'rgba(0, 0, 0, 0.3)', }} > - + ); diff --git a/components/groups/components/__tests__/groupInfo.test.tsx b/components/groups/components/__tests__/groupInfo.test.tsx index 270652a3..d9681776 100644 --- a/components/groups/components/__tests__/groupInfo.test.tsx +++ b/components/groups/components/__tests__/groupInfo.test.tsx @@ -92,12 +92,12 @@ describe('GroupInfo', () => { expect(screen.getByText('No threshold available')).toBeInTheDocument(); }); - test('triggers update modal on button click', () => { + test('triggers upgrade modal on button click', () => { renderWithProps(); - const updateButton = screen.getByLabelText('update-btn'); + const updateButton = screen.getByLabelText('upgrade-btn'); fireEvent.click(updateButton); const modal = document.getElementById(`update-group-modal`) as HTMLDialogElement; expect(modal).toBeInTheDocument(); - expect(screen.getByLabelText('update-group-btn')).toBeInTheDocument(); + expect(screen.getByLabelText('upgrade-group-btn')).toBeInTheDocument(); }); }); diff --git a/components/groups/components/__tests__/myGroups.test.tsx b/components/groups/components/__tests__/myGroups.test.tsx index c0f1d8b5..e59a87be 100644 --- a/components/groups/components/__tests__/myGroups.test.tsx +++ b/components/groups/components/__tests__/myGroups.test.tsx @@ -29,7 +29,7 @@ mock.module('@/hooks/useQueries', () => ({ isBalanceLoading: false, isBalanceError: false, }), - useGetFilteredTxAndSuccessfulProposals: jest.fn().mockReturnValue({ + useGetMessagesFromAddress: jest.fn().mockReturnValue({ sendTxs: [], totalPages: 1, isLoading: false, diff --git a/components/groups/components/groupControls.tsx b/components/groups/components/groupControls.tsx index 35015196..1e67696e 100644 --- a/components/groups/components/groupControls.tsx +++ b/components/groups/components/groupControls.tsx @@ -17,12 +17,13 @@ import { useChain } from '@cosmos-kit/react'; import { MemberSDKType } from '@liftedinit/manifestjs/dist/codegen/cosmos/group/v1/types'; import { ArrowRightIcon } from '@/components/icons'; import ProfileAvatar from '@/utils/identicon'; -import { HistoryBox, TransactionGroup } from '@/components'; +import { HistoryBox } from '@/components'; import { TokenList } from '@/components'; import { CombinedBalanceInfo, ExtendedMetadataSDKType } from '@/utils'; import DenomList from '@/components/factory/components/DenomList'; import { useResponsivePageSize } from '@/hooks/useResponsivePageSize'; import env from '@/config/env'; +import { TxMessage } from '@/components/bank/types'; type GroupControlsProps = { policyAddress: string; @@ -32,7 +33,7 @@ type GroupControlsProps = { isLoading: boolean; currentPage: number; setCurrentPage: React.Dispatch>; - sendTxs: TransactionGroup[]; + sendTxs: TxMessage[]; totalPages: number; txLoading: boolean; isError: boolean; diff --git a/components/groups/components/myGroups.tsx b/components/groups/components/myGroups.tsx index 91204c4f..1582296c 100644 --- a/components/groups/components/myGroups.tsx +++ b/components/groups/components/myGroups.tsx @@ -1,7 +1,7 @@ import { ExtendedGroupType, ExtendedQueryGroupsByMemberResponseSDKType, - useGetFilteredTxAndSuccessfulProposals, + useGetMessagesFromAddress, useTokenBalances, useTokenBalancesResolved, useTokenFactoryDenomsFromAdmin, @@ -192,7 +192,7 @@ export function YourGroups({ isLoading: txLoading, isError, refetch: refetchHistory, - } = useGetFilteredTxAndSuccessfulProposals( + } = useGetMessagesFromAddress( env.indexerUrl, selectedGroup?.policies[0]?.address ?? '', currentPageGroupInfo, diff --git a/components/groups/modals/groupInfo.tsx b/components/groups/modals/groupInfo.tsx index 5e8cdd52..5f3b09d6 100644 --- a/components/groups/modals/groupInfo.tsx +++ b/components/groups/modals/groupInfo.tsx @@ -228,7 +228,7 @@ export function GroupInfo({ +
member?.member?.address === address); + const closeDrawer = () => { const drawer = document.getElementById('my-drawer') as HTMLInputElement; if (drawer) drawer.checked = false; @@ -88,7 +103,7 @@ export default function MobileNav() {
- + {isMember && }
diff --git a/components/react/modal.tsx b/components/react/modal.tsx index 05eeedd9..82bbfb58 100644 --- a/components/react/modal.tsx +++ b/components/react/modal.tsx @@ -255,7 +255,7 @@ export const TailwindModal: React.FC< }) .catch(error => { console.error('Wallet connection error:', error); - // Always keep QRCode view but update its state for these errors + // Always keep QRCode view but upgrade its state for these errors if (isWalletConnectionError(error?.message)) { setQRState(State.Error); setQrMessage(error.message); diff --git a/components/react/sideNav.tsx b/components/react/sideNav.tsx index 0900bd57..4b21a7c3 100644 --- a/components/react/sideNav.tsx +++ b/components/react/sideNav.tsx @@ -19,6 +19,9 @@ import { import { MdContacts } from 'react-icons/md'; import { getRealLogo } from '@/utils'; import env from '@/config/env'; +import { useGroupsByAdmin } from '@/hooks'; +import { usePoaGetAdmin } from '@/hooks'; +import { useChain } from '@cosmos-kit/react'; interface SideNavProps { isDrawerVisible: boolean; @@ -28,6 +31,17 @@ interface SideNavProps { export default function SideNav({ isDrawerVisible, setDrawerVisible }: SideNavProps) { const { toggleTheme, theme } = useTheme(); const [isContactsOpen, setContactsOpen] = useState(false); + const { address } = useChain(env.chain); + + const { poaAdmin } = usePoaGetAdmin(); + + const { groupByAdmin } = useGroupsByAdmin( + poaAdmin ?? 'manifest1afk9zr2hn2jsac63h4hm60vl9z3e5u69gndzf7c99cqge3vzwjzsfmy9qj' + ); + + const group = groupByAdmin?.groups?.[0]; + + const isMember = group?.members?.some(member => member?.member?.address === address); const toggleDrawer = () => setDrawerVisible(!isDrawerVisible); const version = packageInfo.version; @@ -72,7 +86,8 @@ export default function SideNav({ isDrawerVisible, setDrawerVisible }: SideNavPr
    - + {isMember && } +
@@ -144,7 +159,7 @@ export default function SideNav({ isDrawerVisible, setDrawerVisible }: SideNavPr
    - + {isMember && }
diff --git a/hooks/useQueries.ts b/hooks/useQueries.ts index f85693f3..86d383f5 100644 --- a/hooks/useQueries.ts +++ b/hooks/useQueries.ts @@ -13,10 +13,10 @@ import { GroupMemberSDKType, GroupPolicyInfoSDKType, } from '@liftedinit/manifestjs/dist/codegen/cosmos/group/v1/types'; -import { TransactionGroup } from '@/components'; import { Octokit } from 'octokit'; import { useOsmosisRpcQueryClient } from '@/hooks/useOsmosisRpcQueryClient'; +import { TxMessage } from '@/components/bank/types'; export type ExtendedGroupType = QueryGroupsByMemberResponseSDKType['groups'][0] & { policies: GroupPolicyInfoSDKType[]; @@ -862,66 +862,23 @@ const _formatMessage = ( } }; -const transformTransactions = (tx: any, address: string) => { - let messages: TransactionGroup[] = []; - let memo = tx.data.tx.body.memo ? { memo: tx.data.tx.body.memo } : {}; - - for (const message of tx.data.tx.body.messages) { - if (message['@type'] === '/cosmos.group.v1.MsgSubmitProposal') { - for (const nestedMessage of message.messages) { - // Skip the message if it doesn't contain the address - // At least one of the nested messages should contain the address - if (!JSON.stringify(nestedMessage).includes(address)) { - continue; - } - - const formattedMessages = _formatMessage(nestedMessage, address); - for (const formattedMessage of formattedMessages) { - messages.push({ - tx_hash: tx.id, - block_number: parseInt(tx.data.txResponse.height), - formatted_date: tx.data.txResponse.timestamp, - ...memo, - ...formattedMessage, - }); - } - } - } - - const formattedMessages = _formatMessage(message, address); - for (const formattedMessage of formattedMessages) { - messages.push({ - tx_hash: tx.id, - block_number: parseInt(tx.data.txResponse.height), - formatted_date: tx.data.txResponse.timestamp, - ...memo, - ...formattedMessage, - }); - } - } - - return messages; -}; - -// Helper function to transform API response to match the component's expected format -export const useGetFilteredTxAndSuccessfulProposals = ( +export const useGetMessagesFromAddress = ( indexerUrl: string, address: string, page: number = 1, pageSize: number = 10 ) => { - const fetchTransactions = async () => { - const baseUrl = `${indexerUrl}/rpc/get_address_filtered_transactions_and_successful_proposals?address=${address}`; + const fetchMessages = async () => { + const baseUrl = `${indexerUrl}/rpc/get_messages_for_address?_address=${address}`; // Update order parameter to sort by timestamp instead of height const offset = (page - 1) * pageSize; const paginationParams = `&limit=${pageSize}&offset=${offset}`; - const orderParam = `&order=data->txResponse->timestamp.desc`; // Changed from height to timestamp + const orderParam = `&order=timestamp.desc`; const finalUrl = `${baseUrl}${orderParam}${paginationParams}`; try { - // First, get the total count const countResponse = await axios.get(baseUrl, { headers: { Prefer: 'count=exact', @@ -942,18 +899,13 @@ export const useGetFilteredTxAndSuccessfulProposals = ( }, }); - const transactions = dataResponse.data - .flatMap((tx: any) => transformTransactions(tx, address)) - .filter((tx: any) => tx !== null) - // Add secondary JS sort - .sort((a: any, b: any) => { - // Sort by timestamp descending (newest first) - const dateComparison = - new Date(b.formatted_date).getTime() - new Date(a.formatted_date).getTime(); - if (dateComparison !== 0) return dateComparison; - // If timestamps are equal, sort by block number descending - return b.block_number - a.block_number; - }); + const transactions = dataResponse.data.sort((a: TxMessage, b: TxMessage) => { + // Sort by timestamp descending (newest first) + const dateComparison = new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime(); + if (dateComparison !== 0) return dateComparison; + // If timestamps are equal, sort by block number descending + return b.height - a.height; + }); return { transactions, @@ -967,8 +919,8 @@ export const useGetFilteredTxAndSuccessfulProposals = ( }; const sendQuery = useQuery({ - queryKey: ['getFilteredTxsAndSuccessfulProposals', address, page, pageSize], - queryFn: fetchTransactions, + queryKey: ['getMessagesForAddress', address, page, pageSize], + queryFn: fetchMessages, enabled: !!address, }); @@ -982,6 +934,7 @@ export const useGetFilteredTxAndSuccessfulProposals = ( refetch: sendQuery.refetch, }; }; + export const useMultipleTallyCounts = (proposalIds: bigint[]) => { const { lcdQueryClient } = useLcdQueryClient(); diff --git a/package.json b/package.json index a0da39a8..44b4eb59 100644 --- a/package.json +++ b/package.json @@ -32,12 +32,13 @@ "**/@cosmjs/stargate": "npm:@liftedinit/stargate@0.32.4-ll.3" }, "dependencies": { - "@chain-registry/assets": "^1.70.67", + "@chain-registry/assets": "^1.70.93", "@cosmjs/cosmwasm-stargate": "0.32.4", "@cosmjs/stargate": "npm:@liftedinit/stargate@0.32.4-ll.3", "@cosmos-kit/cosmos-extension-metamask": "^0.12.3", + "@cosmos-kit/ledger": "^2.13.3", "@cosmos-kit/react": "2.21.2", - "@cosmos-kit/web3auth": "^2.14.3", + "@cosmos-kit/web3auth": "^2.14.4", "@fontsource/manrope": "^5.0.21", "@headlessui/react": "^1.7.18", "@heroicons/react": "^2.1.5", @@ -53,9 +54,9 @@ "apexcharts": "^3.54.0", "autoprefixer": "^10.4.20", "babel-plugin-glsl": "^1.0.0", - "chain-registry": "1.69.67", + "chain-registry": "1.69.93", "cosmjs-types": "^0.9.0", - "cosmos-kit": "2.23.4", + "cosmos-kit": "^2.23.5", "country-flag-icons": "^1.5.13", "daisyui": "^4.12.10", "dayjs": "^1.11.13", @@ -77,6 +78,7 @@ "react-icons": "^5.3.0", "react-intersection-observer": "^9.13.1", "react-scroll": "^1.9.0", + "react-string-format": "^1.2.0", "react-syntax-highlighter": "^15.6.1", "tailwind-scrollbar-hide": "^2.0.0", "tailwindcss": "^3.4.10", diff --git a/pages/_app.tsx b/pages/_app.tsx index 2529507a..2a595376 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -42,8 +42,7 @@ import { ibcProtoRegistry, } from '@liftedinit/manifestjs'; import MobileNav from '@/components/react/mobileNav'; - -import { OPENLOGIN_NETWORK_TYPE } from '@toruslabs/openlogin-utils'; +import { WEB3AUTH_NETWORK_TYPE } from '@web3auth/auth'; import { SkipProvider } from '@/contexts/skipGoContext'; @@ -153,7 +152,7 @@ function ManifestApp({ Component, pageProps }: ManifestAppProps) { client: { clientId: env.web3AuthClientId, - web3AuthNetwork: env.web3AuthNetwork as OPENLOGIN_NETWORK_TYPE, // Safe to cast since we validate the env vars in config/env.ts + web3AuthNetwork: env.web3AuthNetwork as WEB3AUTH_NETWORK_TYPE, // Safe to cast since we validate the env vars in config/env.ts }, promptSign: async (_, signData) => new Promise(resolve => diff --git a/pages/bank.tsx b/pages/bank.tsx index 91f12bcd..ab1c737c 100644 --- a/pages/bank.tsx +++ b/pages/bank.tsx @@ -1,7 +1,7 @@ import { WalletNotConnected, HistoryBox, SearchIcon } from '@/components'; import { TokenList } from '@/components/bank/components/tokenList'; import { - useGetFilteredTxAndSuccessfulProposals, + useGetMessagesFromAddress, useIsMobile, useOsmosisTokenBalancesResolved, useOsmosisTokenFactoryDenomsMetadata, @@ -92,7 +92,8 @@ export default function Bank() { isLoading: txLoading, isError, refetch: refetchHistory, - } = useGetFilteredTxAndSuccessfulProposals( + totalCount, + } = useGetMessagesFromAddress( env.indexerUrl, chains.manifesttestnet.address ?? '', currentPage, diff --git a/tests/mock.ts b/tests/mock.ts index b4bd926d..af0168d6 100644 --- a/tests/mock.ts +++ b/tests/mock.ts @@ -5,7 +5,7 @@ import { } from '@liftedinit/manifestjs/dist/codegen/cosmos/staking/v1beta1/staking'; import { ExtendedValidatorSDKType, TransactionGroup } from '@/components'; import { CombinedBalanceInfo } from '@/utils/types'; -import { ExtendedGroupType, HistoryTxType } from '@/hooks'; +import { ExtendedGroupType } from '@/hooks'; import { MemberSDKType, ProposalExecutorResult, @@ -18,6 +18,15 @@ import { FormData, ProposalFormData } from '@/helpers'; import { cosmos } from '@liftedinit/manifestjs'; import { Any } from '@liftedinit/manifestjs/dist/codegen/google/protobuf/any'; import { MsgSend } from '@liftedinit/manifestjs/dist/codegen/cosmos/bank/v1beta1/tx'; +import { + MsgBurn, + MsgMint, +} from '@liftedinit/manifestjs/dist/codegen/osmosis/tokenfactory/v1beta1/tx'; +import { + MsgBurnHeldBalance, + MsgPayout, +} from '@liftedinit/manifestjs/dist/codegen/liftedinit/manifest/v1/tx'; +import { TxMessage } from '@/components/bank/types'; export const manifestAddr1 = 'manifest1hj5fveer5cjtn4wd6wstzugjfdxzl0xp8ws9ct'; export const manifestAddr2 = 'manifest1efd63aw40lxf3n4mhf7dzhjkr453axurm6rp3z'; @@ -209,165 +218,171 @@ export const defaultChain: Chain = { }, }; -export const osmosisChain: Chain = { - chain_name: 'osmosistestnet', - chain_id: 'osmo-test-5', - status: 'live', - network_type: 'testnet', - pretty_name: 'Osmosis Testnet', - bech32_prefix: 'osmo', - slip44: 118, - fees: { - fee_tokens: [ - { - denom: 'uosmo', - fixed_min_gas_price: 0.001, - low_gas_price: 0.001, - average_gas_price: 0.001, - high_gas_price: 0.001, - }, - ], - }, -}; - -export const mockTransactions: TransactionGroup[] = [ - // Send +export const mockTransactions: TxMessage[] = [ { - tx_hash: 'hash1', - block_number: 1, - formatted_date: '2023-05-01T12:00:00Z', - data: { - tx_type: HistoryTxType.SEND, - from_address: 'address1', - to_address: 'address2', + id: '1', + message_index: 1, + type: MsgSend.typeUrl, + sender: 'address1', + mentions: ['mention1'], + metadata: { amount: [{ amount: '1000000000000000000000000', denom: 'utoken' }], + toAddress: 'address2', }, + fee: { amount: [{ amount: '1', denom: 'denom1' }], gas: '1' }, + memo: 'memo1', + height: 1, + timestamp: 'timestamp1', + error: '', + proposal_ids: ['proposal1'], }, - // Receive { - tx_hash: 'hash2', - block_number: 2, - formatted_date: '2023-05-02T12:00:00Z', - data: { - tx_type: HistoryTxType.SEND, - from_address: 'address2', - to_address: 'address1', + id: '2', + message_index: 2, + type: MsgSend.typeUrl, + sender: 'address2', + mentions: [], + metadata: { amount: [{ amount: '2000000000000000000000', denom: 'utoken' }], + toAddress: 'address1', }, + fee: { amount: [{ amount: '2', denom: 'utoken' }], gas: '1' }, + memo: '', + height: 2, + timestamp: '2023-05-02T12:00:00Z', + error: '', + proposal_ids: [], }, - // Minted { - tx_hash: 'hash3', - block_number: 3, - formatted_date: '2023-05-03T12:00:00Z', - data: { - tx_type: HistoryTxType.MINT, - from_address: 'address1', - to_address: 'address2', - amount: [{ amount: '3000000000000000000', denom: 'utoken' }], + id: '3', + message_index: 3, + type: MsgMint.typeUrl, + sender: 'address1', + mentions: [], + metadata: { + amount: { amount: '3000000000000000000', denom: 'utoken' }, + mintToAddress: 'address2', }, + fee: { amount: [{ amount: '3', denom: 'utoken' }], gas: '1' }, + memo: '', + height: 3, + timestamp: '2023-05-03T12:00:00Z', + error: '', + proposal_ids: [], }, - // Burned { - tx_hash: 'hash4', - block_number: 4, - formatted_date: '2023-05-04T12:00:00Z', - data: { - tx_type: HistoryTxType.BURN, - from_address: 'address2', - to_address: 'address1', - amount: [{ amount: '1200000000000000', denom: 'utoken' }], + id: '4', + message_index: 4, + type: MsgBurn.typeUrl, + sender: 'address2', + mentions: ['address1', 'address2'], + metadata: { + amount: { amount: '1200000000000000', denom: 'utoken' }, + burnFromAddress: 'address1', }, + fee: { amount: [{ amount: '4', denom: 'utoken' }], gas: '1' }, + memo: '', + height: 4, + timestamp: '2023-05-04T12:00:00Z', + error: '', + proposal_ids: [], }, - // Minted { - tx_hash: 'hash5', - block_number: 5, - formatted_date: '2023-05-05T12:00:00Z', - data: { - tx_type: HistoryTxType.PAYOUT, - from_address: 'address1', - to_address: 'address2', - amount: [{ amount: '5000000000000', denom: 'utoken' }], + id: '5', + message_index: 5, + type: MsgPayout.typeUrl, + sender: 'address1', + mentions: [], + metadata: { + payoutPairs: [{ coin: { amount: '5000000000000', denom: 'utoken' }, address: 'address2' }], }, + fee: { amount: [{ amount: '5', denom: 'utoken' }], gas: '1' }, + memo: '', + height: 5, + timestamp: '2023-05-05T12:00:00Z', + error: '', + proposal_ids: [], }, - // Burned { - tx_hash: 'hash6', - block_number: 6, - formatted_date: '2023-05-06T12:00:00Z', - data: { - tx_type: HistoryTxType.BURN_HELD_BALANCE, - from_address: 'address2', - to_address: 'address1', - amount: [{ amount: '2100000', denom: 'utoken' }], - }, + id: '6', + message_index: 6, + type: MsgBurnHeldBalance.typeUrl, + sender: 'address2', + mentions: [], + metadata: { burnCoins: [{ amount: '2100000', denom: 'utoken' }] }, + fee: { amount: [{ amount: '6', denom: 'utoken' }], gas: '1' }, + memo: '', + height: 6, + timestamp: '2023-05-06T12:00:00Z', + error: '', + proposal_ids: [], }, - // Minted { - tx_hash: 'hash7', - block_number: 7, - formatted_date: '2023-05-07T12:00:00Z', - data: { - tx_type: HistoryTxType.PAYOUT, - from_address: 'address2', - to_address: 'address1', - amount: [{ amount: '2300000', denom: 'utoken' }], + id: '7', + message_index: 7, + type: MsgPayout.typeUrl, + sender: 'address2', + mentions: [], + metadata: { + payoutPairs: [{ coin: { amount: '2300000', denom: 'utoken' }, address: 'address1' }], }, + fee: { amount: [{ amount: '7', denom: 'utoken' }], gas: '1' }, + memo: '', + height: 7, + timestamp: '2023-05-07T12:00:00Z', + error: '', + proposal_ids: [], }, - // Minted { - tx_hash: 'hash8', - block_number: 8, - formatted_date: '2023-05-08T12:00:00Z', - data: { - tx_type: HistoryTxType.PAYOUT, - from_address: 'address2', - to_address: 'address1', - amount: [{ amount: '2400000', denom: 'utoken' }], + id: '8', + message_index: 8, + type: MsgPayout.typeUrl, + sender: 'address2', + mentions: [], + metadata: { + payoutPairs: [{ coin: { amount: '2400000', denom: 'utoken' }, address: 'address1' }], }, + fee: { amount: [{ amount: '8', denom: 'utoken' }], gas: '1' }, + memo: '', + height: 8, + timestamp: '2023-05-08T12:00:00Z', + error: '', + proposal_ids: [], }, - // Minted { - tx_hash: 'hash9', - block_number: 9, - formatted_date: '2023-05-09T12:00:00Z', - data: { - tx_type: HistoryTxType.PAYOUT, - from_address: 'address2', - to_address: 'address1', - amount: [{ amount: '2500000', denom: 'utoken' }], + id: '9', + message_index: 9, + type: MsgPayout.typeUrl, + sender: 'address2', + mentions: [], + metadata: { + payoutPairs: [{ coin: { amount: '2500000', denom: 'utoken' }, address: 'address1' }], }, + fee: { amount: [{ amount: '9', denom: 'utoken' }], gas: '1' }, + memo: '', + height: 9, + timestamp: '2023-05-09T12:00:00Z', + error: '', + proposal_ids: [], }, - // Minted { - tx_hash: 'hash10', - block_number: 10, - formatted_date: '2023-05-10T12:00:00Z', - data: { - tx_type: HistoryTxType.PAYOUT, - from_address: 'address2', - to_address: 'address1', - amount: [{ amount: '2600000', denom: 'utoken' }], + id: '10', + message_index: 10, + type: MsgPayout.typeUrl, + sender: 'address2', + mentions: [], + metadata: { + payoutPairs: [{ coin: { amount: '2600000', denom: 'utoken' }, address: 'address1' }], }, + fee: { amount: [{ amount: '1', denom: 'utoken' }], gas: '1' }, + memo: '', + height: 10, + timestamp: '2023-05-10T12:00:00Z', + error: '', + proposal_ids: [], }, ]; -export const mockStakingParams: ParamsSDKType = { - unbonding_time: { seconds: 86400n, nanos: 0 }, - max_validators: 100, - bond_denom: 'upoa', - min_commission_rate: '0.05', - max_entries: 7, - historical_entries: 200, -}; - -// TODO: Not compatible with alpha.12 as poaParams is not defined in the current version -export const mockPoaParams = { - admins: ['admin1'], - allow_validator_self_exit: true, -}; - export const mockGroup: ExtendedGroupType = { id: 1n, admin: 'admin1', diff --git a/utils/format.ts b/utils/format.ts new file mode 100644 index 00000000..24bc741a --- /dev/null +++ b/utils/format.ts @@ -0,0 +1,46 @@ +import { MetadataSDKType } from '@liftedinit/manifestjs/dist/codegen/cosmos/bank/v1beta1/bank'; +import { shiftDigits } from '@/utils/maths'; + +export function formatLargeNumber(num: number): string { + if (!Number.isFinite(num)) return 'Invalid number'; + if (num === 0) return '0'; + + const quintillion = 1e18; + const quadrillion = 1e15; + const trillion = 1e12; + const billion = 1e9; + const million = 1e6; + + if (num < million) { + return num.toString(); + } + + if (num >= quintillion) { + return `${(num / quintillion).toFixed(2)}QT`; + } else if (num >= quadrillion) { + return `${(num / quadrillion).toFixed(2)}Q`; + } else if (num >= trillion) { + return `${(num / trillion).toFixed(2)}T`; + } else if (num >= billion) { + return `${(num / billion).toFixed(2)}B`; + } else if (num >= million) { + return `${(num / million).toFixed(2)}M`; + } + return num.toFixed(6); +} + +export function formatDenom(denom: string): string { + const cleanDenom = denom.replace(/^factory\/[^/]+\//, ''); + + if (cleanDenom.startsWith('u')) { + return cleanDenom.slice(1).toUpperCase(); + } + + return cleanDenom; +} + +export function formatAmount(amount: string, denom: string, metadata?: MetadataSDKType[]) { + const meta = metadata?.find(m => m.base === denom); + const exponent = Number(meta?.denom_units[1]?.exponent) || 6; + return Number(shiftDigits(amount, -exponent)); +} diff --git a/utils/index.ts b/utils/index.ts index 1b5cb06b..4d96337f 100644 --- a/utils/index.ts +++ b/utils/index.ts @@ -10,3 +10,4 @@ export * from './yupExtensions'; export * from './transactionUtils'; export * from './types'; export * from './constants'; +export * from './format'; diff --git a/utils/json.ts b/utils/json.ts new file mode 100644 index 00000000..7a3058ed --- /dev/null +++ b/utils/json.ts @@ -0,0 +1,8 @@ +export function isJsonString(str: string): boolean { + try { + const parsed = JSON.parse(str); + return parsed !== null && (typeof parsed === 'object' || Array.isArray(parsed)); + } catch (e) { + return false; + } +} diff --git a/utils/voting.ts b/utils/voting.ts index c0c5525a..e11e75db 100644 --- a/utils/voting.ts +++ b/utils/voting.ts @@ -1,5 +1,9 @@ import { AssetList } from '@chain-registry/types'; import { assets } from 'chain-registry'; +import { + VoteOption, + voteOptionFromJSON, +} from '@liftedinit/manifestjs/dist/codegen/cosmos/group/v1/types'; export const decodeUint8Arr = (uint8array: Uint8Array | undefined) => { if (!uint8array) return ''; @@ -13,3 +17,18 @@ export const getCoin = (chainName: string) => { const chainAssets = getChainAssets(chainName); return chainAssets?.assets[0]; }; + +export const formatVote = (vote: string) => { + switch (voteOptionFromJSON(vote)) { + case VoteOption.VOTE_OPTION_YES: + return 'YES'; + case VoteOption.VOTE_OPTION_NO: + return 'NO'; + case VoteOption.VOTE_OPTION_ABSTAIN: + return 'ABSTAIN'; + case VoteOption.VOTE_OPTION_NO_WITH_VETO: + return 'NO WITH VETO'; + default: + return 'UNKNOWN'; + } +};