diff --git a/lib/api/puffer-client.ts b/lib/api/puffer-client.ts index 8613283..0ba40aa 100644 --- a/lib/api/puffer-client.ts +++ b/lib/api/puffer-client.ts @@ -20,6 +20,7 @@ import { NucleusAccountantHandler } from '../contracts/handlers/nucleus-accounta import { NucleusAtomicQueueHandler } from '../contracts/handlers/nucleus-atomic-queue-handler'; import { MtwCarrotHandler } from '../contracts/handlers/mtw-carrot-handler'; import { CarrotStakingHandler } from '../contracts/handlers/carrot-staking-handler'; +import { DistributorHandler } from '../contracts/handlers/distributor-handler'; /** * The core class and the main entry point of the Puffer SDK. @@ -57,6 +58,8 @@ export class PufferClient { public mtwCarrot: MtwCarrotHandler; /** Handler for the `CarrotStaker` contract. */ public carrotStaker: CarrotStakingHandler; + /** Handler for the `Distributor` contract. */ + public distributor: DistributorHandler; /** * Create the Puffer Client. @@ -158,6 +161,11 @@ export class PufferClient { this.walletClient, this.publicClient, ); + this.distributor = new DistributorHandler( + chain, + this.walletClient, + this.publicClient, + ); } /** diff --git a/lib/contracts/abis/mainnet/Distributor.ts b/lib/contracts/abis/mainnet/Distributor.ts new file mode 100644 index 0000000..dfc9907 --- /dev/null +++ b/lib/contracts/abis/mainnet/Distributor.ts @@ -0,0 +1,488 @@ +export const Distributor = [ + { inputs: [], stateMutability: 'nonpayable', type: 'constructor' }, + { inputs: [], name: 'InvalidDispute', type: 'error' }, + { inputs: [], name: 'InvalidLengths', type: 'error' }, + { inputs: [], name: 'InvalidProof', type: 'error' }, + { inputs: [], name: 'InvalidUninitializedRoot', type: 'error' }, + { inputs: [], name: 'NoDispute', type: 'error' }, + { inputs: [], name: 'NotGovernor', type: 'error' }, + { inputs: [], name: 'NotTrusted', type: 'error' }, + { inputs: [], name: 'NotWhitelisted', type: 'error' }, + { inputs: [], name: 'UnresolvedDispute', type: 'error' }, + { inputs: [], name: 'ZeroAddress', type: 'error' }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: 'address', + name: 'previousAdmin', + type: 'address', + }, + { + indexed: false, + internalType: 'address', + name: 'newAdmin', + type: 'address', + }, + ], + name: 'AdminChanged', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'beacon', + type: 'address', + }, + ], + name: 'BeaconUpgraded', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'address', name: 'user', type: 'address' }, + { + indexed: true, + internalType: 'address', + name: 'token', + type: 'address', + }, + { + indexed: false, + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + ], + name: 'Claimed', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: 'uint256', + name: '_disputeAmount', + type: 'uint256', + }, + ], + name: 'DisputeAmountUpdated', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: 'uint48', + name: '_disputePeriod', + type: 'uint48', + }, + ], + name: 'DisputePeriodUpdated', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: false, internalType: 'bool', name: 'valid', type: 'bool' }, + ], + name: 'DisputeResolved', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: '_disputeToken', + type: 'address', + }, + ], + name: 'DisputeTokenUpdated', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: 'string', + name: 'reason', + type: 'string', + }, + ], + name: 'Disputed', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: false, internalType: 'uint8', name: 'version', type: 'uint8' }, + ], + name: 'Initialized', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'address', name: 'user', type: 'address' }, + { indexed: false, internalType: 'bool', name: 'isEnabled', type: 'bool' }, + ], + name: 'OperatorClaimingToggled', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'address', name: 'user', type: 'address' }, + { + indexed: true, + internalType: 'address', + name: 'operator', + type: 'address', + }, + { + indexed: false, + internalType: 'bool', + name: 'isWhitelisted', + type: 'bool', + }, + ], + name: 'OperatorToggled', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'token', + type: 'address', + }, + { indexed: true, internalType: 'address', name: 'to', type: 'address' }, + { + indexed: false, + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + ], + name: 'Recovered', + type: 'event', + }, + { anonymous: false, inputs: [], name: 'Revoked', type: 'event' }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: 'bytes32', + name: 'merkleRoot', + type: 'bytes32', + }, + { + indexed: false, + internalType: 'bytes32', + name: 'ipfsHash', + type: 'bytes32', + }, + { + indexed: false, + internalType: 'uint48', + name: 'endOfDisputePeriod', + type: 'uint48', + }, + ], + name: 'TreeUpdated', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'address', name: 'eoa', type: 'address' }, + { indexed: false, internalType: 'bool', name: 'trust', type: 'bool' }, + ], + name: 'TrustedToggled', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'implementation', + type: 'address', + }, + ], + name: 'Upgraded', + type: 'event', + }, + { + inputs: [{ internalType: 'address', name: '', type: 'address' }], + name: 'canUpdateMerkleRoot', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address[]', name: 'users', type: 'address[]' }, + { internalType: 'address[]', name: 'tokens', type: 'address[]' }, + { internalType: 'uint256[]', name: 'amounts', type: 'uint256[]' }, + { internalType: 'bytes32[][]', name: 'proofs', type: 'bytes32[][]' }, + ], + name: 'claim', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: '', type: 'address' }, + { internalType: 'address', name: '', type: 'address' }, + ], + name: 'claimed', + outputs: [ + { internalType: 'uint208', name: 'amount', type: 'uint208' }, + { internalType: 'uint48', name: 'timestamp', type: 'uint48' }, + { internalType: 'bytes32', name: 'merkleRoot', type: 'bytes32' }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'core', + outputs: [{ internalType: 'contract ICore', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'disputeAmount', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'disputePeriod', + outputs: [{ internalType: 'uint48', name: '', type: 'uint48' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'disputeToken', + outputs: [{ internalType: 'contract IERC20', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'string', name: 'reason', type: 'string' }], + name: 'disputeTree', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'disputer', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'endOfDisputePeriod', + outputs: [{ internalType: 'uint48', name: '', type: 'uint48' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getMerkleRoot', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'contract ICore', name: '_core', type: 'address' }, + ], + name: 'initialize', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'lastTree', + outputs: [ + { internalType: 'bytes32', name: 'merkleRoot', type: 'bytes32' }, + { internalType: 'bytes32', name: 'ipfsHash', type: 'bytes32' }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: '', type: 'address' }], + name: 'onlyOperatorCanClaim', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: '', type: 'address' }, + { internalType: 'address', name: '', type: 'address' }, + ], + name: 'operators', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'proxiableUUID', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'tokenAddress', type: 'address' }, + { internalType: 'address', name: 'to', type: 'address' }, + { internalType: 'uint256', name: 'amountToRecover', type: 'uint256' }, + ], + name: 'recoverERC20', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'bool', name: 'valid', type: 'bool' }], + name: 'resolveDispute', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'revokeTree', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'uint256', name: '_disputeAmount', type: 'uint256' }, + ], + name: 'setDisputeAmount', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'uint48', name: '_disputePeriod', type: 'uint48' }, + ], + name: 'setDisputePeriod', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'contract IERC20', + name: '_disputeToken', + type: 'address', + }, + ], + name: 'setDisputeToken', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: 'user', type: 'address' }], + name: 'toggleOnlyOperatorCanClaim', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'user', type: 'address' }, + { internalType: 'address', name: 'operator', type: 'address' }, + ], + name: 'toggleOperator', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: 'eoa', type: 'address' }], + name: 'toggleTrusted', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'tree', + outputs: [ + { internalType: 'bytes32', name: 'merkleRoot', type: 'bytes32' }, + { internalType: 'bytes32', name: 'ipfsHash', type: 'bytes32' }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + components: [ + { internalType: 'bytes32', name: 'merkleRoot', type: 'bytes32' }, + { internalType: 'bytes32', name: 'ipfsHash', type: 'bytes32' }, + ], + internalType: 'struct MerkleTree', + name: '_tree', + type: 'tuple', + }, + ], + name: 'updateTree', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'newImplementation', type: 'address' }, + ], + name: 'upgradeTo', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'newImplementation', type: 'address' }, + { internalType: 'bytes', name: 'data', type: 'bytes' }, + ], + name: 'upgradeToAndCall', + outputs: [], + stateMutability: 'payable', + type: 'function', + }, +]; diff --git a/lib/contracts/addresses.ts b/lib/contracts/addresses.ts index 3cb6bc7..dc31f74 100644 --- a/lib/contracts/addresses.ts +++ b/lib/contracts/addresses.ts @@ -14,6 +14,7 @@ export const CONTRACT_ADDRESSES = { PufferWithdrawalManager: '0xDdA0483184E75a5579ef9635ED14BacCf9d50283', NucleusAtomicQueue: '0x228c44bb4885c6633f4b6c83f14622f37d5112e5', CarrotStaker: '0x99c599227c65132822f0290d9e5b4b0430d6c0d6', + Distributor: '0x3Ef3D8bA38EBe18DB133cEc108f4D14CE00Dd9Ae', }, [Chain.Holesky]: { PufferVault: '0x9196830bB4c05504E0A8475A0aD566AceEB6BeC9', diff --git a/lib/contracts/handlers/distributor-handler.test.ts b/lib/contracts/handlers/distributor-handler.test.ts new file mode 100644 index 0000000..bf0577f --- /dev/null +++ b/lib/contracts/handlers/distributor-handler.test.ts @@ -0,0 +1,297 @@ +import { Chain } from '../../chains/constants'; +import { + setupTestPublicClient, + setupTestWalletClient, +} from '../../../test/setup-test-clients'; +import { mockAccount, testingUtils } from '../../../test/setup-tests'; +import { DistributorHandler } from './distributor-handler'; +import { Distributor } from '../abis/mainnet/Distributor'; +import { CONTRACT_ADDRESSES } from '../addresses'; +import { isHash, padHex } from 'viem'; + +describe('DistributorHandler', () => { + const contractTestingUtils = testingUtils.generateContractUtils( + Distributor, + (CONTRACT_ADDRESSES[Chain.Mainnet] as any).Distributor, + ); + let handler: DistributorHandler; + + beforeEach(() => { + testingUtils.mockConnectedWallet([mockAccount], { chainId: Chain.Mainnet }); + const walletClient = setupTestWalletClient(Chain.Mainnet); + const publicClient = setupTestPublicClient(Chain.Mainnet); + + handler = new DistributorHandler(Chain.Mainnet, walletClient, publicClient); + }); + + it('should check if an address can update merkle root', async () => { + const mockCanUpdate = 1n; + contractTestingUtils.mockCall('canUpdateMerkleRoot', [mockCanUpdate]); + + const canUpdate = await handler.canUpdateMerkleRoot(mockAccount); + expect(canUpdate).toEqual(mockCanUpdate); + }); + + it('should claim tokens for multiple users', async () => { + contractTestingUtils.mockTransaction('claim'); + const mockUsers = [ + padHex('0x1', { size: 20 }), + padHex('0x2', { size: 20 }), + ]; + const mockTokens = [ + padHex('0x3', { size: 20 }), + padHex('0x4', { size: 20 }), + ]; + const mockAmounts = [100n, 200n]; + const mockProofs = [ + [padHex('0x5', { size: 32 })], + [padHex('0x6', { size: 32 })], + ]; + + const txHash = await handler.claim(mockAccount, { + users: mockUsers, + tokens: mockTokens, + amounts: mockAmounts, + proofs: mockProofs, + }); + expect(isHash(txHash)).toBeTruthy(); + }); + + it('should get claim information', async () => { + const mockClaimInfo = { + amount: 100n, + timestamp: 1000n, + merkleRoot: padHex('0x', { size: 32 }), + }; + contractTestingUtils.mockCall('claimed', [ + mockClaimInfo.amount, + Number(mockClaimInfo.timestamp), + mockClaimInfo.merkleRoot, + ]); + + const claimInfo = await handler.claimed( + mockAccount, + padHex('0x', { size: 20 }), + ); + expect(claimInfo).toEqual(mockClaimInfo); + }); + + it('should get the core contract address', async () => { + const mockCore = padHex('0x', { size: 20 }); + contractTestingUtils.mockCall('core', [mockCore]); + + const core = await handler.core(); + expect(core).toEqual(mockCore); + }); + + it('should get the dispute amount', async () => { + const mockDisputeAmount = 100n; + contractTestingUtils.mockCall('disputeAmount', [mockDisputeAmount]); + + const disputeAmount = await handler.disputeAmount(); + expect(disputeAmount).toEqual(mockDisputeAmount); + }); + + it('should get the dispute period', async () => { + const mockDisputePeriod = 100; + contractTestingUtils.mockCall('disputePeriod', [mockDisputePeriod]); + + const disputePeriod = await handler.disputePeriod(); + expect(disputePeriod).toEqual(mockDisputePeriod); + }); + + it('should get the dispute token address', async () => { + const mockDisputeToken = padHex('0x', { size: 20 }); + contractTestingUtils.mockCall('disputeToken', [mockDisputeToken]); + + const disputeToken = await handler.disputeToken(); + expect(disputeToken).toEqual(mockDisputeToken); + }); + + it('should dispute the tree', async () => { + contractTestingUtils.mockTransaction('disputeTree'); + const mockReason = 'Invalid merkle root'; + + const txHash = await handler.disputeTree(mockAccount, mockReason); + expect(isHash(txHash)).toBeTruthy(); + }); + + it('should get the disputer address', async () => { + const mockDisputer = padHex('0x', { size: 20 }); + contractTestingUtils.mockCall('disputer', [mockDisputer]); + + const disputer = await handler.disputer(); + expect(disputer).toEqual(mockDisputer); + }); + + it('should get the end of dispute period', async () => { + const mockEndOfDisputePeriod = 100; + contractTestingUtils.mockCall('endOfDisputePeriod', [ + mockEndOfDisputePeriod, + ]); + + const endOfDisputePeriod = await handler.endOfDisputePeriod(); + expect(endOfDisputePeriod).toEqual(mockEndOfDisputePeriod); + }); + + it('should get the merkle root', async () => { + const mockMerkleRoot = padHex('0x', { size: 32 }); + contractTestingUtils.mockCall('getMerkleRoot', [mockMerkleRoot]); + + const merkleRoot = await handler.getMerkleRoot(); + expect(merkleRoot).toEqual(mockMerkleRoot); + }); + + it('should get the last tree information', async () => { + const mockTree = { + merkleRoot: padHex('0x1', { size: 32 }), + ipfsHash: padHex('0x2', { size: 32 }), + }; + contractTestingUtils.mockCall('lastTree', [ + mockTree.merkleRoot, + mockTree.ipfsHash, + ]); + + const tree = await handler.lastTree(); + expect(tree).toEqual(mockTree); + }); + + it('should check if only operators can claim for a user', async () => { + const mockOnlyOperatorCanClaim = 1n; + contractTestingUtils.mockCall('onlyOperatorCanClaim', [ + mockOnlyOperatorCanClaim, + ]); + + const onlyOperatorCanClaim = + await handler.onlyOperatorCanClaim(mockAccount); + expect(onlyOperatorCanClaim).toEqual(mockOnlyOperatorCanClaim); + }); + + it('should check if an address is an operator for a user', async () => { + const mockIsOperator = 1n; + contractTestingUtils.mockCall('operators', [mockIsOperator]); + + const isOperator = await handler.operators( + mockAccount, + padHex('0x', { size: 20 }), + ); + expect(isOperator).toEqual(mockIsOperator); + }); + + it('should recover ERC20 tokens', async () => { + contractTestingUtils.mockTransaction('recoverERC20'); + const mockTokenAddress = padHex('0x1', { size: 20 }); + const mockTo = padHex('0x2', { size: 20 }); + const mockAmountToRecover = 100n; + + const txHash = await handler.recoverERC20( + mockAccount, + mockTokenAddress, + mockTo, + mockAmountToRecover, + ); + expect(isHash(txHash)).toBeTruthy(); + }); + + it('should resolve a dispute', async () => { + contractTestingUtils.mockTransaction('resolveDispute'); + const mockValid = true; + + const txHash = await handler.resolveDispute(mockAccount, mockValid); + expect(isHash(txHash)).toBeTruthy(); + }); + + it('should revoke the tree', async () => { + contractTestingUtils.mockTransaction('revokeTree'); + + const txHash = await handler.revokeTree(mockAccount); + expect(isHash(txHash)).toBeTruthy(); + }); + + it('should set the dispute amount', async () => { + contractTestingUtils.mockTransaction('setDisputeAmount'); + const mockDisputeAmount = 100n; + + const txHash = await handler.setDisputeAmount( + mockAccount, + mockDisputeAmount, + ); + expect(isHash(txHash)).toBeTruthy(); + }); + + it('should set the dispute period', async () => { + contractTestingUtils.mockTransaction('setDisputePeriod'); + const mockDisputePeriod = 100; + + const txHash = await handler.setDisputePeriod( + mockAccount, + mockDisputePeriod, + ); + expect(isHash(txHash)).toBeTruthy(); + }); + + it('should set the dispute token', async () => { + contractTestingUtils.mockTransaction('setDisputeToken'); + const mockDisputeToken = padHex('0x', { size: 20 }); + + const txHash = await handler.setDisputeToken(mockAccount, mockDisputeToken); + expect(isHash(txHash)).toBeTruthy(); + }); + + it('should toggle only operator can claim', async () => { + contractTestingUtils.mockTransaction('toggleOnlyOperatorCanClaim'); + const mockUser = padHex('0x', { size: 20 }); + + const txHash = await handler.toggleOnlyOperatorCanClaim( + mockAccount, + mockUser, + ); + expect(isHash(txHash)).toBeTruthy(); + }); + + it('should toggle operator', async () => { + contractTestingUtils.mockTransaction('toggleOperator'); + const mockUser = padHex('0x1', { size: 20 }); + const mockOperator = padHex('0x2', { size: 20 }); + + const txHash = await handler.toggleOperator( + mockAccount, + mockUser, + mockOperator, + ); + expect(isHash(txHash)).toBeTruthy(); + }); + + it('should toggle trusted status', async () => { + contractTestingUtils.mockTransaction('toggleTrusted'); + const mockEoa = padHex('0x', { size: 20 }); + + const txHash = await handler.toggleTrusted(mockAccount, mockEoa); + expect(isHash(txHash)).toBeTruthy(); + }); + + it('should get the current tree information', async () => { + const mockTree = { + merkleRoot: padHex('0x1', { size: 32 }), + ipfsHash: padHex('0x2', { size: 32 }), + }; + contractTestingUtils.mockCall('tree', [ + mockTree.merkleRoot, + mockTree.ipfsHash, + ]); + + const tree = await handler.tree(); + expect(tree).toEqual(mockTree); + }); + + it('should update the merkle tree', async () => { + contractTestingUtils.mockTransaction('updateTree'); + const mockTree = { + merkleRoot: padHex('0x1', { size: 32 }), + ipfsHash: padHex('0x2', { size: 32 }), + }; + + const txHash = await handler.updateTree(mockAccount, mockTree); + expect(isHash(txHash)).toBeTruthy(); + }); +}); diff --git a/lib/contracts/handlers/distributor-handler.ts b/lib/contracts/handlers/distributor-handler.ts new file mode 100644 index 0000000..86a3a59 --- /dev/null +++ b/lib/contracts/handlers/distributor-handler.ts @@ -0,0 +1,394 @@ +import { + WalletClient, + PublicClient, + getContract, + Address, + GetContractReturnType, +} from 'viem'; +import { Chain, VIEM_CHAINS, ViemChain } from '../../chains/constants'; +import { CONTRACT_ADDRESSES } from '../addresses'; +import { Distributor } from '../abis/mainnet/Distributor'; + +interface ContractAddressesWithDistributor { + Distributor: Address; +} + +export interface MerkleTree { + merkleRoot: `0x${string}`; + ipfsHash: `0x${string}`; +} + +export interface ClaimInfo { + amount: bigint; + timestamp: bigint; + merkleRoot: `0x${string}`; +} + +export interface ClaimParams { + users: Address[]; + tokens: Address[]; + amounts: bigint[]; + proofs: `0x${string}`[][]; +} + +/** + * Handler for the Merkle Distributor contract, for claiming rewards, e.g. mtwCARROT. + */ +export class DistributorHandler { + private viemChain: ViemChain; + + /** + * Create the handler for the Distributor contract exposing + * methods to interact with the contract. + * + * @param chain Chain to use for the client. + * @param walletClient The wallet client to use for wallet interactions. + * @param publicClient The public client to use for public interactions. + */ + constructor( + private chain: Chain, + private walletClient: WalletClient, + private publicClient: PublicClient, + ) { + this.viemChain = VIEM_CHAINS[chain]; + } + + /** + * Get the contract. + * + * @returns The viem contract. + */ + public getContract() { + const address = ( + CONTRACT_ADDRESSES[ + this.chain + ] as unknown as ContractAddressesWithDistributor + ).Distributor; + const abi = Distributor; + const client = { public: this.publicClient, wallet: this.walletClient }; + + return getContract({ address, abi, client }) as GetContractReturnType< + typeof abi, + typeof client, + Address + >; + } + + /** + * Check if an address can update the merkle root. + * + * @param address The address to check. + * @returns Whether the address can update the merkle root. + */ + public canUpdateMerkleRoot(address: Address) { + return this.getContract().read.canUpdateMerkleRoot([address]); + } + + /** + * Claim tokens for multiple users. + * + * @param account The account making the claim. + * @param params The claim parameters. + * @returns A promise that resolves to the transaction hash. + */ + public claim( + account: Address, + { users, tokens, amounts, proofs }: ClaimParams, + ) { + return this.getContract().write.claim([users, tokens, amounts, proofs], { + account, + chain: this.viemChain, + }); + } + + /** + * Get claim information for a user and token. + * + * @param user The user address. + * @param token The token address. + * @returns The claim information. + */ + public async claimed(user: Address, token: Address): Promise { + const [amount, timestamp, merkleRoot] = + await this.getContract().read.claimed([user, token]); + return { + amount, + timestamp: BigInt(timestamp), + merkleRoot, + }; + } + + /** + * Get the core contract address. + * + * @returns The core contract address. + */ + public core() { + return this.getContract().read.core(); + } + + /** + * Get the dispute amount. + * + * @returns The dispute amount. + */ + public disputeAmount() { + return this.getContract().read.disputeAmount(); + } + + /** + * Get the dispute period. + * + * @returns The dispute period in seconds. + */ + public disputePeriod() { + return this.getContract().read.disputePeriod(); + } + + /** + * Get the dispute token address. + * + * @returns The dispute token address. + */ + public disputeToken() { + return this.getContract().read.disputeToken(); + } + + /** + * Dispute the current tree. + * + * @param account The account disputing the tree. + * @param reason The reason for the dispute. + * @returns A promise that resolves to the transaction hash. + */ + public disputeTree(account: Address, reason: string) { + return this.getContract().write.disputeTree([reason], { + account, + chain: this.viemChain, + }); + } + + /** + * Get the current disputer address. + * + * @returns The disputer address. + */ + public disputer() { + return this.getContract().read.disputer(); + } + + /** + * Get the end of the dispute period. + * + * @returns The timestamp when the dispute period ends. + */ + public endOfDisputePeriod() { + return this.getContract().read.endOfDisputePeriod(); + } + + /** + * Get the current merkle root. + * + * @returns The current merkle root. + */ + public getMerkleRoot() { + return this.getContract().read.getMerkleRoot(); + } + + /** + * Get the last tree information. + * + * @returns The last tree information. + */ + public async lastTree(): Promise { + const [merkleRoot, ipfsHash] = await this.getContract().read.lastTree(); + return { + merkleRoot, + ipfsHash, + }; + } + + /** + * Check if only operators can claim for a user. + * + * @param user The user address to check. + * @returns Whether only operators can claim for the user. + */ + public onlyOperatorCanClaim(user: Address) { + return this.getContract().read.onlyOperatorCanClaim([user]); + } + + /** + * Check if an address is an operator for a user. + * + * @param user The user address. + * @param operator The operator address. + * @returns Whether the address is an operator. + */ + public operators(user: Address, operator: Address) { + return this.getContract().read.operators([user, operator]); + } + + /** + * Recover ERC20 tokens sent to the contract. + * + * @param account The account recovering the tokens. + * @param tokenAddress The token address to recover. + * @param to The address to send recovered tokens to. + * @param amountToRecover The amount to recover. + * @returns A promise that resolves to the transaction hash. + */ + public recoverERC20( + account: Address, + tokenAddress: Address, + to: Address, + amountToRecover: bigint, + ) { + return this.getContract().write.recoverERC20( + [tokenAddress, to, amountToRecover], + { + account, + chain: this.viemChain, + }, + ); + } + + /** + * Resolve a dispute. + * + * @param account The account resolving the dispute. + * @param valid Whether the disputed tree is valid. + * @returns A promise that resolves to the transaction hash. + */ + public resolveDispute(account: Address, valid: boolean) { + return this.getContract().write.resolveDispute([valid], { + account, + chain: this.viemChain, + }); + } + + /** + * Revoke the current tree. + * + * @param account The account revoking the tree. + * @returns A promise that resolves to the transaction hash. + */ + public revokeTree(account: Address) { + return this.getContract().write.revokeTree({ + account, + chain: this.viemChain, + }); + } + + /** + * Set the dispute amount. + * + * @param account The account setting the dispute amount. + * @param disputeAmount The new dispute amount. + * @returns A promise that resolves to the transaction hash. + */ + public setDisputeAmount(account: Address, disputeAmount: bigint) { + return this.getContract().write.setDisputeAmount([disputeAmount], { + account, + chain: this.viemChain, + }); + } + + /** + * Set the dispute period. + * + * @param account The account setting the dispute period. + * @param disputePeriod The new dispute period in seconds. + * @returns A promise that resolves to the transaction hash. + */ + public setDisputePeriod(account: Address, disputePeriod: number) { + return this.getContract().write.setDisputePeriod([disputePeriod], { + account, + chain: this.viemChain, + }); + } + + /** + * Set the dispute token. + * + * @param account The account setting the dispute token. + * @param disputeToken The new dispute token address. + * @returns A promise that resolves to the transaction hash. + */ + public setDisputeToken(account: Address, disputeToken: Address) { + return this.getContract().write.setDisputeToken([disputeToken], { + account, + chain: this.viemChain, + }); + } + + /** + * Toggle whether only operators can claim for a user. + * + * @param account The account toggling the setting. + * @param user The user address to toggle for. + * @returns A promise that resolves to the transaction hash. + */ + public toggleOnlyOperatorCanClaim(account: Address, user: Address) { + return this.getContract().write.toggleOnlyOperatorCanClaim([user], { + account, + chain: this.viemChain, + }); + } + + /** + * Toggle an operator for a user. + * + * @param account The account toggling the operator. + * @param user The user address. + * @param operator The operator address to toggle. + * @returns A promise that resolves to the transaction hash. + */ + public toggleOperator(account: Address, user: Address, operator: Address) { + return this.getContract().write.toggleOperator([user, operator], { + account, + chain: this.viemChain, + }); + } + + /** + * Toggle whether an address is trusted. + * + * @param account The account toggling the trusted status. + * @param eoa The address to toggle trust for. + * @returns A promise that resolves to the transaction hash. + */ + public toggleTrusted(account: Address, eoa: Address) { + return this.getContract().write.toggleTrusted([eoa], { + account, + chain: this.viemChain, + }); + } + + /** + * Get the current tree information. + * + * @returns The current tree information. + */ + public async tree(): Promise { + const [merkleRoot, ipfsHash] = await this.getContract().read.tree(); + return { + merkleRoot, + ipfsHash, + }; + } + + /** + * Update the merkle tree. + * + * @param account The account updating the tree. + * @param tree The new tree information. + * @returns A promise that resolves to the transaction hash. + */ + public updateTree(account: Address, tree: MerkleTree) { + return this.getContract().write.updateTree([tree], { + account, + chain: this.viemChain, + }); + } +} diff --git a/lib/main.ts b/lib/main.ts index 887aa0e..604585f 100644 --- a/lib/main.ts +++ b/lib/main.ts @@ -11,3 +11,4 @@ export * from './contracts/handlers/puffer-depositor-handler'; export * from './contracts/handlers/puffer-l2-depositor-handler'; export * from './contracts/handlers/puffer-vault-handler'; export * from './contracts/handlers/puffer-withdrawal-manager-handler'; +export * from './contracts/handlers/distributor-handler';