Skip to content

Commit

Permalink
feat(rwa): forced transfer functionality (#2775)
Browse files Browse the repository at this point in the history
  • Loading branch information
sstraatemans authored Jan 8, 2025
1 parent 869bb18 commit 6b163f3
Show file tree
Hide file tree
Showing 10 changed files with 187 additions and 60 deletions.
2 changes: 2 additions & 0 deletions .changeset/short-planets-grin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { PartiallyFreezeTokensForm } from '@/components/PartiallyFreezeTokensFor
import { SideBarBreadcrumbs } from '@/components/SideBarBreadcrumbs/SideBarBreadcrumbs';
import { TXTYPES } from '@/components/TransactionsProvider/TransactionsProvider';
import { TransactionTypeSpinner } from '@/components/TransactionTypeSpinner/TransactionTypeSpinner';
import { TransferForm } from '@/components/TransferForm/TransferForm';
import { useAccount } from '@/hooks/account';
import { useAddInvestor } from '@/hooks/addInvestor';
import { useDistributeTokens } from '@/hooks/distributeTokens';
import { useGetInvestor } from '@/hooks/getInvestor';
Expand All @@ -19,6 +21,7 @@ import { useParams } from 'next/navigation';

const InvestorPage = () => {
const params = useParams();
const { accountRoles } = useAccount();
const investorAccount = decodeURIComponent(params.investorAccount as string);
const { isAllowed: isPartiallyFreezeTokensAllowed } =
useTogglePartiallyFreezeTokens({
Expand Down Expand Up @@ -47,6 +50,13 @@ const InvestorPage = () => {
<Stack width="100%" flexDirection="column">
<InvestorInfo account={investor} />
<Stack gap="sm">
{accountRoles.isTransferManager() && (
<TransferForm
investorAccount={investorAccount}
isForced={true}
trigger={<Button variant="warning">Forced transfer</Button>}
/>
)}
<DistributionForm
investorAccount={investorAccount}
trigger={
Expand Down
42 changes: 0 additions & 42 deletions packages/apps/rwa-demo/src/app/(app)/assets/[uuid]/page.tsx

This file was deleted.

23 changes: 17 additions & 6 deletions packages/apps/rwa-demo/src/app/(app)/assets/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { AssetFormScreen } from '@/components/AssetForm/AssetFormScreen';
import { Confirmation } from '@/components/Confirmation/Confirmation';
import { SideBarBreadcrumbs } from '@/components/SideBarBreadcrumbs/SideBarBreadcrumbs';
import { useAccount } from '@/hooks/account';
import { useAsset } from '@/hooks/asset';
import { MonoAdd, MonoDelete, MonoFindInPage } from '@kadena/kode-icons';
import { Button } from '@kadena/kode-ui';
Expand All @@ -18,22 +19,32 @@ import {
SectionCardHeader,
SideBarBreadcrumbsItem,
useLayout,
useNotifications,
} from '@kadena/kode-ui/patterns';
import { useRouter } from 'next/navigation';
import { useState } from 'react';

const Assets = () => {
const { assets, removeAsset } = useAsset();
const { account } = useAccount();
const { assets, removeAsset, setAsset, getAsset } = useAsset();
const { addNotification } = useNotifications();
const [openSide, setOpenSide] = useState(false);
const router = useRouter();
const { setIsRightAsideExpanded, isRightAsideExpanded } = useLayout();

const handleDelete = (value: any) => {
removeAsset(value);
};

const handleLink = async (uuid: any) => {
router.push(`/assets/${uuid}`);
const handleLink = async (assetProp: any) => {
const asset = await getAsset(assetProp.uuid, account!);
if (!asset) {
addNotification({
intent: 'negative',
label: 'asset is not found',
});
return;
}
setAsset(asset);
window.location.href = '/';
};

return (
Expand Down Expand Up @@ -78,7 +89,7 @@ const Assets = () => {
},
{
label: '',
key: 'uuid',
key: '',
width: '10%',
render: CompactTableFormatters.FormatActions({
trigger: (
Expand Down
5 changes: 4 additions & 1 deletion packages/apps/rwa-demo/src/app/(app)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,10 @@ const Home = () => {
}
/>

<TransferForm trigger={<TransferAssetAction />} />
<TransferForm
investorAccount={account?.address!}
trigger={<TransferAssetAction />}
/>
</Stack>
</SectionCardBody>
</SectionCardContentBlock>
Expand Down
59 changes: 49 additions & 10 deletions packages/apps/rwa-demo/src/components/TransferForm/TransferForm.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
import { useAccount } from '@/hooks/account';
import { useForcedTransferTokens } from '@/hooks/forcedTransferTokens';
import { useGetFrozenTokens } from '@/hooks/getFrozenTokens';
import { useGetInvestorBalance } from '@/hooks/getInvestorBalance';
import { useGetInvestors } from '@/hooks/getInvestors';
import { useTransferTokens } from '@/hooks/transferTokens';
import type { ITransferTokensProps } from '@/services/transferTokens';
import { Button, Select, SelectItem, TextField } from '@kadena/kode-ui';
import {
Button,
Notification,
NotificationHeading,
Select,
SelectItem,
Stack,
TextField,
} from '@kadena/kode-ui';
import {
RightAside,
RightAsideContent,
Expand All @@ -19,16 +29,26 @@ import { AssetPausedMessage } from '../AssetPausedMessage/AssetPausedMessage';
interface IProps {
onClose?: () => void;
trigger: ReactElement;
isForced?: boolean;
investorAccount: string;
}

export const TransferForm: FC<IProps> = ({ onClose, trigger }) => {
export const TransferForm: FC<IProps> = ({
onClose,
trigger,
isForced = false,
investorAccount,
}) => {
const [isOpen, setIsOpen] = useState(false);
const { setIsRightAsideExpanded, isRightAsideExpanded } = useLayout();
const { account, balance } = useAccount();
const { account } = useAccount();
const { data: balance } = useGetInvestorBalance({ investorAccount });
const { data: investors } = useGetInvestors();
const { submit: forcedSubmit, isAllowed: isForcedAllowed } =
useForcedTransferTokens();
const { submit, isAllowed } = useTransferTokens();
const { data: frozenAmount } = useGetFrozenTokens({
investorAccount: account?.address!,
investorAccount,
});

const {
Expand All @@ -39,8 +59,9 @@ export const TransferForm: FC<IProps> = ({ onClose, trigger }) => {
} = useForm<ITransferTokensProps>({
values: {
amount: 0,
investorFromAccount: account?.address!,
investorFromAccount: investorAccount,
investorToAccount: '',
isForced,
},
});

Expand All @@ -57,17 +78,20 @@ export const TransferForm: FC<IProps> = ({ onClose, trigger }) => {
};

const onSubmit = async (data: ITransferTokensProps) => {
await submit(data);
if (data.isForced) {
await forcedSubmit(data);
} else {
await submit(data);
}
handleOnClose();
};

const filteredInvestors = investors.filter(
(i) => i.accountName !== account?.address,
(i) => i.accountName !== investorAccount,
);

if (!account) return;

const maxAmount = balance - frozenAmount;
const maxAmount = isForced ? balance : balance - frozenAmount;

return (
<>
Expand All @@ -76,6 +100,16 @@ export const TransferForm: FC<IProps> = ({ onClose, trigger }) => {
<form onSubmit={handleSubmit(onSubmit)}>
<RightAsideHeader label="Transfer Tokens" />
<RightAsideContent>
{isForced && (
<Stack width="100%" marginBlockEnd="md">
<Notification role="status" intent="warning">
<NotificationHeading>Warning</NotificationHeading>
This is a forced transfer (partial frozen tokens can also be
transfered)
</Notification>
</Stack>
)}
<input type="hidden" {...register('isForced', {})} />
<TextField
label="Amount"
type="number"
Expand Down Expand Up @@ -128,7 +162,12 @@ export const TransferForm: FC<IProps> = ({ onClose, trigger }) => {
<Button onPress={handleOnClose} variant="transparent">
Cancel
</Button>
<Button isDisabled={!isAllowed || !isValid} type="submit">
<Button
isDisabled={
(isForced ? !isForcedAllowed : !isAllowed) || !isValid
}
type="submit"
>
Transfer
</Button>
</RightAsideFooter>
Expand Down
6 changes: 5 additions & 1 deletion packages/apps/rwa-demo/src/hooks/distributeTokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { useEffect, useState } from 'react';
import { useAccount } from './account';
import { useAsset } from './asset';
import { useFreeze } from './freeze';
import { useGetInvestorBalance } from './getInvestorBalance';
import { useTransactions } from './transactions';

export const useDistributeTokens = ({
Expand All @@ -22,6 +23,7 @@ export const useDistributeTokens = ({
const { paused, asset } = useAsset();

const { account, sign, accountRoles, isMounted } = useAccount();
const { data: investorBalance } = useGetInvestorBalance({ investorAccount });
const { addTransaction, isActiveAccountChangeTx } = useTransactions();
const { addNotification } = useNotifications();
const [isAllowed, setIsAllowed] = useState(false);
Expand Down Expand Up @@ -62,7 +64,8 @@ export const useDistributeTokens = ({
asset.maxSupply === INFINITE_COMPLIANCE) &&
((asset.maxInvestors > INFINITE_COMPLIANCE &&
asset.maxInvestors > asset.investorCount) ||
asset.maxInvestors === INFINITE_COMPLIANCE),
asset.maxInvestors === INFINITE_COMPLIANCE ||
investorBalance > 0),
);
}, [
frozen,
Expand All @@ -72,6 +75,7 @@ export const useDistributeTokens = ({
accountRoles,
isActiveAccountChangeTx,
asset,
investorBalance,
]);

return { submit, isAllowed };
Expand Down
54 changes: 54 additions & 0 deletions packages/apps/rwa-demo/src/hooks/forcedTransferTokens.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import {
interpretErrorMessage,
TXTYPES,
} from '@/components/TransactionsProvider/TransactionsProvider';
import { forcedTransferTokens } from '@/services/forcedTransferTokens';
import type { ITransferTokensProps } from '@/services/transferTokens';
import { getClient } from '@/utils/client';
import { useNotifications } from '@kadena/kode-ui/patterns';
import { useEffect, useState } from 'react';
import { useAccount } from './account';
import { useAsset } from './asset';
import { useTransactions } from './transactions';

export const useForcedTransferTokens = () => {
const { account, sign, isMounted, accountRoles } = useAccount();
const { paused } = useAsset();
const { addTransaction } = useTransactions();
const { addNotification } = useNotifications();
const [isAllowed, setIsAllowed] = useState(false);

const submit = async (data: ITransferTokensProps) => {
try {
const tx = await forcedTransferTokens(data, account!);
const signedTransaction = await sign(tx);
if (!signedTransaction) return;

const client = getClient();
const res = await client.submit(signedTransaction);

return addTransaction({
...res,
type: TXTYPES.TRANSFERTOKENS,
accounts: [
account?.address!,
data.investorFromAccount,
data.investorToAccount,
],
});
} catch (e: any) {
addNotification({
intent: 'negative',
label: 'there was an error',
message: interpretErrorMessage(e.message),
});
}
};

useEffect(() => {
if (!isMounted) return;
setIsAllowed(!paused && accountRoles.isTransferManager());
}, [paused, isMounted, accountRoles]);

return { submit, isAllowed };
};
45 changes: 45 additions & 0 deletions packages/apps/rwa-demo/src/services/forcedTransferTokens.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import type { IWalletAccount } from '@/components/AccountProvider/AccountType';
import { getNetwork } from '@/utils/client';
import { getAsset } from '@/utils/getAsset';
import { getPubkeyFromAccount } from '@/utils/getPubKey';
import { Pact } from '@kadena/client';
import { PactNumber } from '@kadena/pactjs';
import type { ITransferTokensProps } from './transferTokens';

const createPubKeyFromAccount = (account: string): string => {
return account.replace('k:', '').replace('r:', '');
};

export const forcedTransferTokens = async (
data: ITransferTokensProps,
account: IWalletAccount,
) => {
return Pact.builder
.execution(
`
(${getAsset()}.forced-transfer (read-string 'investorFrom) (read-string 'investorTo) ${new PactNumber(data.amount).toDecimal()})`,
)
.addData('agent', account.address)
.addData('investorFrom', data.investorFromAccount)
.addData('investorTo', data.investorToAccount)
.setMeta({
senderAccount: account.address,
chainId: getNetwork().chainId,
})
.addSigner(createPubKeyFromAccount(account.address), (withCap) => [
withCap(`${getAsset()}.ONLY-AGENT`, 'transfer-manager'),
withCap(
`${getAsset()}.TRANSFER`,
data.investorFromAccount,
data.investorToAccount,
{
decimal: data.amount,
},
),
])
.addSigner(getPubkeyFromAccount(account), (withCap) => [
withCap(`coin.GAS`),
])
.setNetworkId(getNetwork().networkId)
.createTransaction();
};
Loading

0 comments on commit 6b163f3

Please sign in to comment.