Skip to content

Commit 6e6709d

Browse files
committed
Added enhanements
1 parent d4d8bcb commit 6e6709d

File tree

7 files changed

+135
-6
lines changed

7 files changed

+135
-6
lines changed

README.md

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,19 @@ This section maps each business requirement to its implementation so the BA can
7272
| Deposit with sub-type breakdown | Supported | `addTokensDetailed(programId, amount, rewardType, note, subTypeIds, subTypeQtys)` — validates sub-type IDs exist and quantities sum to the deposit amount. Emits `DepositSubTypes` event. |
7373
| View sub-types | Supported | `getSubTypes(programId, rewardType)` returns all active sub-type IDs and names. |
7474

75+
### Transfer Control Limit
76+
77+
| Requirement | Status | Implementation |
78+
|-------------|--------|----------------|
79+
| Program owner specifies transfer control limit (0-100%) | Supported | `setTransferLimit(programId, limitPercent)` — admin sets a per-program percentage. Stored on-chain as `uint8`. |
80+
| Limit restricts Client transfers to parent | Supported | Enforced in `_transferToParentCore`. If SRP limit is 50% and client has 1200 points, max transfer is 600. To transfer 500, client must have at least 1000. |
81+
| Limit does NOT apply to TeamLeader or ProgramAdmin | Supported | Only `MemberRole.Client` is checked. Higher roles transfer freely. |
82+
| 0 = no restriction (default) | Supported | Default value is `0` (Solidity zero-init). Existing programs are unaffected — fully backward compatible. |
83+
| Different programs can have different limits | Supported | Per-program mapping: `mapping(uint32 => uint8) _transferLimits`. SRP at 50%, another at 25%, etc. |
84+
| Admin can view current limit | Supported | `getTransferLimit(programId)` returns the current percentage. Displayed on Programs detail page. |
85+
| UI shows transfer limit info | Supported | Programs page displays limit (e.g., "50%" or "None"). Tokens and Balance pages show info alert with max transferable amount when limit > 0. |
86+
| Admin can change limit | Supported | "Set Transfer Limit" button on Programs detail page (admin only) opens dialog with 0-100 input. |
87+
7588
### QA10 — Data Intelligence Reports
7689

7790
| Requirement | Status | Implementation |
@@ -150,6 +163,15 @@ Members can transfer tokens back up the hierarchy to any parent in their chain.
150163

151164
Deduction order: available balance -> expired time-locks -> unexpired time-locks -> permanently locked.
152165

166+
#### Transfer Control Limit
167+
Each program can have a **Transfer Control Limit** (0-100%) that restricts how much a **Client** can transfer to their parent. The limit is calculated as a percentage of the client's **total balance** (available + permanently locked + time-locked).
168+
169+
- **0% (default)**: No restriction — backward compatible with existing programs
170+
- **50%**: Client must have at least 2x the transfer amount (e.g., 1200 balance → max 600 transfer)
171+
- **100%**: Client can transfer their entire balance
172+
- Only applies to **Client** role — TeamLeaders and ProgramAdmins are not restricted
173+
- Set by Admin via `setTransferLimit(programId, limitPercent)`
174+
153175
### Withdraw
154176
Members withdraw available (unlocked) tokens to their wallet. Expired time-locks are automatically resolved during withdrawal.
155177

@@ -183,7 +205,8 @@ Overview showing total programs count and a summary table of all programs (ID, C
183205

184206
### Programs (`/programs`)
185207
- **List view**: Table of all programs with ID, code, name, description, and status
186-
- **Detail view** (`/programs?id=1`): Program details, your balance breakdown, and member table with columns: Member ID, Wallet, Role, **Type**, Parent, Balance, Status, QR
208+
- **Detail view** (`/programs?id=1`): Program details (including **Transfer Limit**), your balance breakdown, and member table with columns: Member ID, Wallet, Role, **Type**, Parent, Balance, Status, QR
209+
- **Set Transfer Limit** (Admin only): Dialog to set the per-program transfer control limit (0-100%)
187210
- **Create Program** (Admin only): Dialog to create a new program with code, name, and description
188211
- **Add Program Admin** (Admin only): Assign a ProgramAdmin with wallet, member ID, and **member type**
189212
- **Add Member** (ProgramAdmin/TeamLeader): Add TeamLeaders or Clients with role and **member type** selection
@@ -199,7 +222,7 @@ Results show: Member ID, Wallet, Role, **Type**, Program, Parent, Balance, Statu
199222
Four-tab interface for token operations:
200223
1. **Deposit**: Approve and deposit FULA tokens with **reward type** selection and **note** field (128 chars)
201224
2. **Transfer to Sub-Member**: Send tokens to a sub-member with optional lock
202-
3. **Transfer to Parent**: Return tokens to a parent in the hierarchy
225+
3. **Transfer to Parent**: Return tokens to a parent in the hierarchy. Shows transfer limit info alert when the program has a limit configured.
203226
4. **Withdraw**: Withdraw available tokens to your wallet
204227

205228
### Balance Lookup (`/balance`)
@@ -209,7 +232,7 @@ Public page that does **not require wallet connection** for viewing. Enter a Mem
209232
- Balance breakdown (withdrawable, locked, time-locked) per program
210233
- Status in each program
211234

212-
If the connected wallet matches the member's wallet, action panels appear for deposit (with **reward type** and **note**), transfer to parent, and withdraw.
235+
If the connected wallet matches the member's wallet, action panels appear for deposit (with **reward type** and **note**), transfer to parent (with **transfer limit** info), and withdraw.
213236

214237
Shareable link format: `/balance?member=MEMBER_ID`
215238

src/app/balance/page.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { useSearchParams } from "next/navigation";
1111
import { zeroAddress } from "viem";
1212
import { CONTRACTS, REWARDS_PROGRAM_ABI, MemberRoleLabels, MemberTypeLabels } from "@/config/contracts";
1313
import { toBytes12, fromBytes12, fromBytes8, shortenAddress, formatFula, formatContractError, fromBytes16 } from "@/lib/utils";
14-
import { useProgramCount, useProgram, useTransferToParent, useWithdraw, useApproveToken, useAddTokens, useRewardTypes } from "@/hooks/useRewardsProgram";
14+
import { useProgramCount, useProgram, useTransferToParent, useWithdraw, useApproveToken, useAddTokens, useRewardTypes, useTransferLimit } from "@/hooks/useRewardsProgram";
1515
import { OnChainDisclaimer } from "@/components/common/OnChainDisclaimer";
1616
import { QRCodeDisplay } from "@/components/common/QRCodeDisplay";
1717
import { QRScannerButton } from "@/components/common/QRScannerButton";
@@ -77,6 +77,7 @@ function OwnerActions({ memberWallet }: { memberWallet: string }) {
7777
const [parentTo, setParentTo] = useState("");
7878
const [parentAmount, setParentAmount] = useState("");
7979
const { transferBack, isPending: isTransBack, isConfirming: isTransBackConf, isSuccess: transBackSuccess, error: transBackError } = useTransferToParent();
80+
const { data: transferLimitData } = useTransferLimit(pid);
8081

8182
// Withdraw
8283
const [withdrawAmount, setWithdrawAmount] = useState("");
@@ -138,6 +139,11 @@ function OwnerActions({ memberWallet }: { memberWallet: string }) {
138139
{/* Transfer to Parent */}
139140
<Grid item xs={12} md={4}>
140141
<Typography variant="subtitle2" gutterBottom>Transfer to Parent</Typography>
142+
{transferLimitData != null && Number(transferLimitData) > 0 && (
143+
<Alert severity="info" sx={{ mb: 1 }}>
144+
Transfer limit: <strong>{Number(transferLimitData)}%</strong> of total balance (Clients only).
145+
</Alert>
146+
)}
141147
<TextField label="Parent Wallet (optional)" value={parentTo} onChange={(e) => setParentTo(e.target.value)}
142148
fullWidth size="small" placeholder="0x... (empty = direct parent)" />
143149
<TextField label="Amount (FULA)" value={parentAmount} onChange={(e) => setParentAmount(e.target.value)}

src/app/programs/page.tsx

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { useUserRole, useMemberRole } from "@/hooks/useUserRole";
1515
import {
1616
useProgramCount, useProgram, useCreateProgram,
1717
useAssignProgramAdmin, useAddMember, useMemberBalance,
18+
useTransferLimit, useSetTransferLimit,
1819
} from "@/hooks/useRewardsProgram";
1920
import { CONTRACTS, REWARDS_PROGRAM_ABI, MemberRoleLabels, MemberRoleEnum, MemberTypeLabels } from "@/config/contracts";
2021
import { fromBytes8, fromBytes12, shortenAddress, formatFula, isValidAddress, formatContractError } from "@/lib/utils";
@@ -99,9 +100,13 @@ function ProgramDetail({ programId }: { programId: number }) {
99100

100101
const { assignProgramAdmin, isPending: isPendingPA, isConfirming: isConfirmingPA, isSuccess: isSuccessPA, error: errorPA } = useAssignProgramAdmin();
101102
const { addMember, isPending: isPendingM, isConfirming: isConfirmingM, isSuccess: isSuccessM, error: errorM } = useAddMember();
103+
const { data: transferLimit } = useTransferLimit(programId);
104+
const { setTransferLimit, isPending: isPendingTL, isConfirming: isConfirmingTL, isSuccess: isSuccessTL, error: errorTL } = useSetTransferLimit();
102105

103106
const [openPA, setOpenPA] = useState(false);
104107
const [openMember, setOpenMember] = useState(false);
108+
const [openTL, setOpenTL] = useState(false);
109+
const [tlValue, setTlValue] = useState("");
105110
const [paWallet, setPaWallet] = useState("");
106111
const [paMemberId, setPaMemberId] = useState("");
107112
const [mWallet, setMWallet] = useState("");
@@ -144,7 +149,7 @@ function ProgramDetail({ programId }: { programId: number }) {
144149
<Box>
145150
<Typography variant="h4">{program.name}</Typography>
146151
<Typography color="text.secondary">
147-
Code: {fromBytes8(program.code as `0x${string}`)} | ID: {program.id}
152+
Code: {fromBytes8(program.code as `0x${string}`)} | ID: {program.id} | Transfer Limit: {transferLimit && Number(transferLimit) > 0 ? `${transferLimit}%` : "None"}
148153
</Typography>
149154
<Typography color="text.secondary">{program.description}</Typography>
150155
</Box>
@@ -155,6 +160,11 @@ function ProgramDetail({ programId }: { programId: number }) {
155160
Add Program Admin
156161
</Button>
157162
)}
163+
{isAdmin && (
164+
<Button variant="outlined" onClick={() => { setTlValue(String(transferLimit && Number(transferLimit) > 0 ? transferLimit : "")); setOpenTL(true); }}>
165+
Set Transfer Limit
166+
</Button>
167+
)}
158168
{canAddMembers && (
159169
<Button variant="outlined" onClick={() => setOpenMember(true)}>
160170
Add Member
@@ -292,6 +302,29 @@ function ProgramDetail({ programId }: { programId: number }) {
292302
</Button>
293303
</DialogActions>
294304
</Dialog>
305+
306+
{/* Set Transfer Limit Dialog */}
307+
<Dialog open={openTL} onClose={() => setOpenTL(false)} maxWidth="xs" fullWidth>
308+
<DialogTitle>Set Transfer Limit</DialogTitle>
309+
<DialogContent>
310+
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
311+
Set the maximum percentage of balance a Client can transfer to their parent. Set to 0 for no limit.
312+
</Typography>
313+
<TextField label="Limit (%)" value={tlValue} onChange={(e) => setTlValue(e.target.value)}
314+
fullWidth margin="normal" type="number" inputProps={{ min: 0, max: 100 }}
315+
helperText="0 = no restriction, 50 = max 50% of balance" />
316+
{errorTL && <Alert severity="error" sx={{ mt: 2 }}>{formatContractError(errorTL)}</Alert>}
317+
{isSuccessTL && <Alert severity="success" sx={{ mt: 2 }}>Transfer limit updated!</Alert>}
318+
</DialogContent>
319+
<DialogActions>
320+
<Button onClick={() => setOpenTL(false)}>Cancel</Button>
321+
<Button variant="contained"
322+
onClick={() => setTransferLimit(programId, parseInt(tlValue) || 0)}
323+
disabled={isPendingTL || isConfirmingTL}>
324+
{isPendingTL || isConfirmingTL ? <CircularProgress size={20} /> : "Set Limit"}
325+
</Button>
326+
</DialogActions>
327+
</Dialog>
295328
</Box>
296329
);
297330
}

src/app/tokens/page.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { readContract } from "wagmi/actions";
1111
import { zeroAddress } from "viem";
1212
import {
1313
useApproveToken, useAddTokens, useTransferToSubMember, useTransferToParent,
14-
useWithdraw, useMemberBalance, useTokenBalance, useRewardTypes,
14+
useWithdraw, useMemberBalance, useTokenBalance, useRewardTypes, useTransferLimit,
1515
} from "@/hooks/useRewardsProgram";
1616
import { formatFula, toBytes12, isValidAddress, formatContractError, fromBytes16 } from "@/lib/utils";
1717
import { OnChainDisclaimer } from "@/components/common/OnChainDisclaimer";
@@ -36,6 +36,9 @@ export default function TokensPage() {
3636
// Reward types
3737
const { data: rewardTypesData } = useRewardTypes();
3838

39+
// Transfer limit
40+
const { data: transferLimitData } = useTransferLimit(pid);
41+
3942
// Deposit state
4043
const [depositAmount, setDepositAmount] = useState("");
4144
const [depositRewardType, setDepositRewardType] = useState(0);
@@ -219,6 +222,14 @@ export default function TokensPage() {
219222
<Typography variant="body2" color="text.secondary" gutterBottom>
220223
Leave wallet empty to transfer to your direct parent.
221224
</Typography>
225+
{transferLimitData != null && Number(transferLimitData) > 0 && (
226+
<Alert severity="info" sx={{ mb: 2 }}>
227+
This program limits Client transfers to <strong>{Number(transferLimitData)}%</strong> of total balance.
228+
{balance && (
229+
<> Your max transferable: <strong>{formatFula((balance[0] + balance[1] + balance[2]) * BigInt(Number(transferLimitData)) / BigInt(100))} FULA</strong></>
230+
)}
231+
</Alert>
232+
)}
222233
<TextField label="Parent Wallet (optional)" value={parentTo} onChange={(e) => setParentTo(e.target.value)}
223234
fullWidth margin="normal" placeholder="0x... (leave empty for direct parent)" />
224235
<TextField label="Amount (FULA)" value={parentAmount} onChange={(e) => setParentAmount(e.target.value)}

src/config/contracts.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,33 @@ export const REWARDS_PROGRAM_ABI = [
532532
{ name: "quantities", type: "uint128[]", indexed: false },
533533
],
534534
},
535+
// Transfer Control Limit
536+
{
537+
name: "setTransferLimit",
538+
type: "function",
539+
stateMutability: "nonpayable",
540+
inputs: [
541+
{ name: "programId", type: "uint32" },
542+
{ name: "limitPercent", type: "uint8" },
543+
],
544+
outputs: [],
545+
},
546+
{
547+
name: "getTransferLimit",
548+
type: "function",
549+
stateMutability: "view",
550+
inputs: [{ name: "programId", type: "uint32" }],
551+
outputs: [{ type: "uint8" }],
552+
},
553+
{
554+
name: "TransferLimitUpdated",
555+
type: "event",
556+
inputs: [
557+
{ name: "programId", type: "uint32", indexed: true },
558+
{ name: "oldLimit", type: "uint8", indexed: false },
559+
{ name: "newLimit", type: "uint8", indexed: false },
560+
],
561+
},
535562
] as const;
536563

537564
// ERC20 ABI (minimal)

src/hooks/useRewardsProgram.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,33 @@ export function useAddSubType() {
340340
return { addSubType, isPending, isConfirming, isSuccess, error, hash };
341341
}
342342

343+
export function useTransferLimit(programId: number) {
344+
return useReadContract({
345+
address: CONTRACTS.rewardsProgram,
346+
abi: REWARDS_PROGRAM_ABI,
347+
functionName: "getTransferLimit",
348+
args: [programId],
349+
query: { enabled: programId > 0 },
350+
});
351+
}
352+
353+
export function useSetTransferLimit() {
354+
const { writeContract, data: hash, isPending, error } = useWriteContract();
355+
const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({ hash });
356+
useRefetchOnSuccess(isSuccess);
357+
358+
const setTransferLimit = (programId: number, limitPercent: number) => {
359+
writeContract({
360+
address: CONTRACTS.rewardsProgram,
361+
abi: REWARDS_PROGRAM_ABI,
362+
functionName: "setTransferLimit",
363+
args: [programId, limitPercent],
364+
});
365+
};
366+
367+
return { setTransferLimit, isPending, isConfirming, isSuccess, error, hash };
368+
}
369+
343370
export function useAddTokensDetailed() {
344371
const { writeContract, data: hash, isPending, error } = useWriteContract();
345372
const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({ hash });

src/lib/utils.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ const ERROR_MAP: Record<string, string> = {
8484
InvalidMemberType: "Invalid member type.",
8585
InvalidRewardType: "Invalid reward type.",
8686
InvalidSubTypeData: "Invalid sub-type data.",
87+
TransferExceedsLimit: "Transfer amount exceeds the program's transfer control limit.",
88+
InvalidTransferLimit: "Transfer limit must be between 0 and 100.",
8789
InvalidAddress: "Invalid address provided.",
8890
};
8991

0 commit comments

Comments
 (0)