diff --git a/pkgs/contract/.openzeppelin/sepolia.json b/pkgs/contract/.openzeppelin/sepolia.json index 3f01793..deb586b 100644 --- a/pkgs/contract/.openzeppelin/sepolia.json +++ b/pkgs/contract/.openzeppelin/sepolia.json @@ -25,6 +25,11 @@ "address": "0x08B4c53b98f46B14E2AD00189C2Aa3b9F3d0c8f3", "txHash": "0x0b7d0cacd542bf718e6d99c13f12690b07a0f3228c31d013a2aa399cb67ae04e", "kind": "transparent" + }, + { + "address": "0x9e70310b99F946dB10394575c0B611A741e0A5db", + "txHash": "0xbbec6d941bc359657ce025f8122b6bfa1b7b57ae00ba261f7baea44e79830f95", + "kind": "transparent" } ], "impls": { @@ -1253,6 +1258,316 @@ ] } } + }, + "bc6865de0e474b9423cb88c57d2cef5b0f4f1fc2f79c90bdca4b42b3bedc216a": { + "address": "0xe1a14D26a6cc879616fE569ed5708322D3Ed8Ff9", + "txHash": "0x1ea91babffeff4c0e5286768511fd6827e08b493fd382374d09293979cf73f6b", + "layout": { + "solcVersion": "0.8.24", + "storage": [ + { + "label": "SPLITS_CREATOR_IMPLEMENTATION", + "offset": 0, + "slot": "0", + "type": "t_address", + "contract": "SplitsCreatorFactory", + "src": "contracts/splitscreator/SplitsCreatorFactory.sol:20" + }, + { + "label": "BIG_BANG", + "offset": 0, + "slot": "1", + "type": "t_address", + "contract": "SplitsCreatorFactory", + "src": "contracts/splitscreator/SplitsCreatorFactory.sol:22" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_struct(InitializableStorage)73_storage": { + "label": "struct Initializable.InitializableStorage", + "members": [ + { + "label": "_initialized", + "type": "t_uint64", + "offset": 0, + "slot": "0" + }, + { + "label": "_initializing", + "type": "t_bool", + "offset": 8, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(OwnableStorage)13_storage": { + "label": "struct OwnableUpgradeable.OwnableStorage", + "members": [ + { + "label": "_owner", + "type": "t_address", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_uint64": { + "label": "uint64", + "numberOfBytes": "8" + } + }, + "namespaces": { + "erc7201:openzeppelin.storage.Ownable": [ + { + "contract": "OwnableUpgradeable", + "label": "_owner", + "type": "t_address", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:24", + "offset": 0, + "slot": "0" + } + ], + "erc7201:openzeppelin.storage.Initializable": [ + { + "contract": "Initializable", + "label": "_initialized", + "type": "t_uint64", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:69", + "offset": 0, + "slot": "0" + }, + { + "contract": "Initializable", + "label": "_initializing", + "type": "t_bool", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:73", + "offset": 8, + "slot": "0" + } + ] + } + } + }, + "dd12bf6e75fb0acd68de953a2d3bb52bb98ef7fc45c7771ef4b96e40a51ea298": { + "address": "0x30Ee73482782dA4300C60B1DcD30b429a9C5f1eA", + "txHash": "0xb0055890b9abcbb62d1763d3fdf5d97615985dd3e6759714092a954c9840cd10", + "layout": { + "solcVersion": "0.8.24", + "storage": [ + { + "label": "TOKEN_SUPPLY", + "offset": 0, + "slot": "0", + "type": "t_uint256", + "contract": "FractionToken", + "src": "contracts/fractiontoken/FractionToken.sol:15" + }, + { + "label": "tokenRecipients", + "offset": 0, + "slot": "1", + "type": "t_mapping(t_uint256,t_array(t_address)dyn_storage)", + "contract": "FractionToken", + "src": "contracts/fractiontoken/FractionToken.sol:17" + }, + { + "label": "hatsContract", + "offset": 0, + "slot": "2", + "type": "t_contract(IHats)17350", + "contract": "FractionToken", + "src": "contracts/fractiontoken/FractionToken.sol:19" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_mapping(t_address,t_bool))": { + "label": "mapping(address => mapping(address => bool))", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_uint256)": { + "label": "mapping(address => uint256)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_mapping(t_address,t_uint256))": { + "label": "mapping(uint256 => mapping(address => uint256))", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_uint256)": { + "label": "mapping(uint256 => uint256)", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(ERC1155Storage)166_storage": { + "label": "struct ERC1155Upgradeable.ERC1155Storage", + "members": [ + { + "label": "_balances", + "type": "t_mapping(t_uint256,t_mapping(t_address,t_uint256))", + "offset": 0, + "slot": "0" + }, + { + "label": "_operatorApprovals", + "type": "t_mapping(t_address,t_mapping(t_address,t_bool))", + "offset": 0, + "slot": "1" + }, + { + "label": "_uri", + "type": "t_string_storage", + "offset": 0, + "slot": "2" + } + ], + "numberOfBytes": "96" + }, + "t_struct(ERC1155SupplyStorage)449_storage": { + "label": "struct ERC1155SupplyUpgradeable.ERC1155SupplyStorage", + "members": [ + { + "label": "_totalSupply", + "type": "t_mapping(t_uint256,t_uint256)", + "offset": 0, + "slot": "0" + }, + { + "label": "_totalSupplyAll", + "type": "t_uint256", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_struct(InitializableStorage)73_storage": { + "label": "struct Initializable.InitializableStorage", + "members": [ + { + "label": "_initialized", + "type": "t_uint64", + "offset": 0, + "slot": "0" + }, + { + "label": "_initializing", + "type": "t_bool", + "offset": 8, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint64": { + "label": "uint64", + "numberOfBytes": "8" + }, + "t_array(t_address)dyn_storage": { + "label": "address[]", + "numberOfBytes": "32" + }, + "t_contract(IHats)17350": { + "label": "contract IHats", + "numberOfBytes": "20" + }, + "t_mapping(t_uint256,t_array(t_address)dyn_storage)": { + "label": "mapping(uint256 => address[])", + "numberOfBytes": "32" + } + }, + "namespaces": { + "erc7201:openzeppelin.storage.ERC1155Supply": [ + { + "contract": "ERC1155SupplyUpgradeable", + "label": "_totalSupply", + "type": "t_mapping(t_uint256,t_uint256)", + "src": "@openzeppelin/contracts-upgradeable/token/ERC1155/extensions/ERC1155SupplyUpgradeable.sol:25", + "offset": 0, + "slot": "0" + }, + { + "contract": "ERC1155SupplyUpgradeable", + "label": "_totalSupplyAll", + "type": "t_uint256", + "src": "@openzeppelin/contracts-upgradeable/token/ERC1155/extensions/ERC1155SupplyUpgradeable.sol:26", + "offset": 0, + "slot": "1" + } + ], + "erc7201:openzeppelin.storage.ERC1155": [ + { + "contract": "ERC1155Upgradeable", + "label": "_balances", + "type": "t_mapping(t_uint256,t_mapping(t_address,t_uint256))", + "src": "@openzeppelin/contracts-upgradeable/token/ERC1155/ERC1155Upgradeable.sol:27", + "offset": 0, + "slot": "0" + }, + { + "contract": "ERC1155Upgradeable", + "label": "_operatorApprovals", + "type": "t_mapping(t_address,t_mapping(t_address,t_bool))", + "src": "@openzeppelin/contracts-upgradeable/token/ERC1155/ERC1155Upgradeable.sol:29", + "offset": 0, + "slot": "1" + }, + { + "contract": "ERC1155Upgradeable", + "label": "_uri", + "type": "t_string_storage", + "src": "@openzeppelin/contracts-upgradeable/token/ERC1155/ERC1155Upgradeable.sol:32", + "offset": 0, + "slot": "2" + } + ], + "erc7201:openzeppelin.storage.Initializable": [ + { + "contract": "Initializable", + "label": "_initialized", + "type": "t_uint64", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:69", + "offset": 0, + "slot": "0" + }, + { + "contract": "Initializable", + "label": "_initializing", + "type": "t_bool", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:73", + "offset": 8, + "slot": "0" + } + ] + } + } } } } diff --git a/pkgs/contract/contracts/fractiontoken/FractionToken.sol b/pkgs/contract/contracts/fractiontoken/FractionToken.sol index 217c750..80fec56 100644 --- a/pkgs/contract/contracts/fractiontoken/FractionToken.sol +++ b/pkgs/contract/contracts/fractiontoken/FractionToken.sol @@ -2,11 +2,16 @@ pragma solidity ^0.8.24; import { ERC1155Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC1155/ERC1155Upgradeable.sol"; +import { ERC1155SupplyUpgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC1155/extensions/ERC1155SupplyUpgradeable.sol"; import { IHats } from "../hats/src/Interfaces/IHats.sol"; import { IFractionToken } from "./IFractionToken.sol"; import { IERC1155 } from "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; -contract FractionToken is ERC1155Upgradeable, IFractionToken { +contract FractionToken is + ERC1155Upgradeable, + ERC1155SupplyUpgradeable, + IFractionToken +{ uint256 public TOKEN_SUPPLY; mapping(uint256 => address[]) private tokenRecipients; @@ -192,9 +197,31 @@ contract FractionToken is ERC1155Upgradeable, IFractionToken { return balances; } + function totalSupply( + address wearer, + uint256 hatId + ) public view returns (uint256) { + uint256 tokenId = getTokenId(hatId, wearer); + + if (tokenRecipients[tokenId].length == 0) { + return TOKEN_SUPPLY; + } + + return super.totalSupply(tokenId); + } + function uri( uint256 tokenId ) public view override(ERC1155Upgradeable) returns (string memory) { return super.uri(tokenId); } + + function _update( + address from, + address to, + uint256[] memory ids, + uint256[] memory values + ) internal virtual override(ERC1155Upgradeable, ERC1155SupplyUpgradeable) { + super._update(from, to, ids, values); + } } diff --git a/pkgs/contract/contracts/fractiontoken/IFractionToken.sol b/pkgs/contract/contracts/fractiontoken/IFractionToken.sol index f5c38de..aeabbfe 100644 --- a/pkgs/contract/contracts/fractiontoken/IFractionToken.sol +++ b/pkgs/contract/contracts/fractiontoken/IFractionToken.sol @@ -46,4 +46,9 @@ interface IFractionToken is IERC1155 { address[] memory warers, uint256[] memory hatIds ) external view returns (uint256[] memory); + + function totalSupply( + address wearer, + uint256 hatId + ) external view returns (uint256); } diff --git a/pkgs/contract/contracts/fractiontoken/mock/FractionToken_Mock_v2.sol b/pkgs/contract/contracts/fractiontoken/mock/FractionToken_Mock_v2.sol index df4e677..1ba6b89 100644 --- a/pkgs/contract/contracts/fractiontoken/mock/FractionToken_Mock_v2.sol +++ b/pkgs/contract/contracts/fractiontoken/mock/FractionToken_Mock_v2.sol @@ -1,11 +1,12 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; -import "@openzeppelin/contracts-upgradeable/token/ERC1155/ERC1155Upgradeable.sol"; +import { ERC1155Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC1155/ERC1155Upgradeable.sol"; +import { ERC1155SupplyUpgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC1155/extensions/ERC1155SupplyUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; import { IHats } from "../../hats/src/Interfaces/IHats.sol"; -contract FractionToken_Mock_v2 is ERC1155Upgradeable { +contract FractionToken_Mock_v2 is ERC1155Upgradeable, ERC1155SupplyUpgradeable { uint256 public TOKEN_SUPPLY; mapping(uint256 => address[]) private tokenRecipients; @@ -202,6 +203,15 @@ contract FractionToken_Mock_v2 is ERC1155Upgradeable { return super._contextSuffixLength(); } + function _update( + address from, + address to, + uint256[] memory ids, + uint256[] memory values + ) internal virtual override(ERC1155Upgradeable, ERC1155SupplyUpgradeable) { + super._update(from, to, ids, values); + } + /** * 検証用に追加した関数 */ diff --git a/pkgs/contract/contracts/splitscreator/SplitsCreator.sol b/pkgs/contract/contracts/splitscreator/SplitsCreator.sol index fc75aed..63cb5c9 100644 --- a/pkgs/contract/contracts/splitscreator/SplitsCreator.sol +++ b/pkgs/contract/contracts/splitscreator/SplitsCreator.sol @@ -68,27 +68,23 @@ contract SplitsCreator is ISplitsCreator, Clone { * @notice Previews the allocations without creating a split contract. * @param _splitsInfo An array of SplitsInfo structs containing details about roles, wearers, and multipliers. * @return shareHolders An array of shareholder addresses. - * @return percentages Corresponding allocation percentages (with 18 decimals) for each shareholder. + * @return allocations Corresponding allocations for each shareholder. + * @return totalAllocation Sum of all allocations. */ function preview( SplitsInfo[] memory _splitsInfo ) external view - returns (address[] memory shareHolders, uint256[] memory percentages) - { - ( - address[] memory _shareHolders, + returns ( + address[] memory shareHolders, uint256[] memory allocations, uint256 totalAllocation - ) = _calculateAllocations(_splitsInfo); - - percentages = new uint256[](allocations.length); - for (uint256 i = 0; i < allocations.length; i++) { - percentages[i] = (allocations[i] * 1e18) / totalAllocation; - } - - return (_shareHolders, percentages); + ) + { + (shareHolders, allocations, totalAllocation) = _calculateAllocations( + _splitsInfo + ); } /** @@ -113,80 +109,98 @@ contract SplitsCreator is ISplitsCreator, Clone { for (uint256 i = 0; i < _splitsInfo.length; i++) { SplitsInfo memory _splitInfo = _splitsInfo[i]; for (uint256 si = 0; si < _splitInfo.wearers.length; si++) { - uint256 tokenId = FRACTION_TOKEN().getTokenId( - _splitInfo.hatId, - _splitInfo.wearers[si] - ); address[] memory recipients = FRACTION_TOKEN() - .getTokenRecipients(tokenId); - numOfShareHolders += recipients.length; + .getTokenRecipients( + FRACTION_TOKEN().getTokenId( + _splitInfo.hatId, + _splitInfo.wearers[si] + ) + ); + if (recipients.length == 0) { + numOfShareHolders++; + } else { + numOfShareHolders += recipients.length; + } } } shareHolders = new address[](numOfShareHolders); - address[] memory wearers = new address[](numOfShareHolders); - uint256[] memory hatIdsOfShareHolders = new uint256[]( - numOfShareHolders - ); - uint256[] memory roleMultipliersOfShareHolders = new uint256[]( - numOfShareHolders - ); - uint256[] memory hatsTimeFrameMultipliersOfShareHolders = new uint256[]( - numOfShareHolders - ); - + allocations = new uint256[](numOfShareHolders); uint256 shareHolderIndex = 0; - for (uint256 i = 0; i < _splitsInfo.length; i++) { + for (uint i = 0; i < _splitsInfo.length; i++) { SplitsInfo memory _splitInfo = _splitsInfo[i]; + uint256 roleMultiplier = _splitInfo.multiplierTop / _splitInfo.multiplierBottom; - for (uint256 si = 0; si < _splitInfo.wearers.length; si++) { - address wearer = _splitInfo.wearers[si]; - require( - HATS().balanceOf(wearer, _splitInfo.hatId) > 0, - "Invalid wearer" + uint256 fractionTokenSupply = 0; + uint256 currentShareHolderIndex = shareHolderIndex; + for (uint j = 0; j < _splitInfo.wearers.length; j++) { + uint256 hatsTimeFrameMultiplier = _getHatsTimeFrameMultiplier( + _splitInfo.wearers[j], + _splitInfo.hatId ); - uint256 tokenId = FRACTION_TOKEN().getTokenId( - _splitInfo.hatId, - wearer + fractionTokenSupply += FRACTION_TOKEN().totalSupply( + _splitInfo.wearers[j], + _splitInfo.hatId ); - uint256 hatsTimeFrameMultiplier = _getHatsTimeFrameMultiplier( - wearer, + + uint256 wearerBalance = FRACTION_TOKEN().balanceOf( + _splitInfo.wearers[j], + _splitInfo.wearers[j], _splitInfo.hatId ); - // Get the recipients from FractionToken + uint256 wearerScore = wearerBalance * + roleMultiplier * + hatsTimeFrameMultiplier; + + shareHolders[shareHolderIndex] = _splitInfo.wearers[j]; + allocations[shareHolderIndex] = wearerScore; + + shareHolderIndex++; + address[] memory recipients = FRACTION_TOKEN() - .getTokenRecipients(tokenId); - for (uint256 j = 0; j < recipients.length; j++) { - shareHolders[shareHolderIndex] = recipients[j]; - wearers[shareHolderIndex] = wearer; - hatIdsOfShareHolders[shareHolderIndex] = _splitInfo.hatId; - roleMultipliersOfShareHolders[ - shareHolderIndex - ] = roleMultiplier; - hatsTimeFrameMultipliersOfShareHolders[ - shareHolderIndex - ] = hatsTimeFrameMultiplier; + .getTokenRecipients( + FRACTION_TOKEN().getTokenId( + _splitInfo.hatId, + _splitInfo.wearers[j] + ) + ); + + for (uint k = 0; k < recipients.length; k++) { + if (recipients[k] == _splitInfo.wearers[j]) continue; + + uint256 recipientBalance = FRACTION_TOKEN().balanceOf( + recipients[k], + _splitInfo.wearers[j], + _splitInfo.hatId + ); + + uint256 recipientScore = recipientBalance * + roleMultiplier * + hatsTimeFrameMultiplier; + + shareHolders[shareHolderIndex] = recipients[k]; + allocations[shareHolderIndex] = recipientScore; shareHolderIndex++; } } - } - uint256[] memory balanceOfShareHolders = FRACTION_TOKEN() - .balanceOfBatch(shareHolders, wearers, hatIdsOfShareHolders); + for (uint l = 0; l < allocations.length; l++) { + if (l >= currentShareHolderIndex && l < shareHolderIndex) { + allocations[l] = + (allocations[l] * 10e5) / + fractionTokenSupply; + } + } + } totalAllocation = 0; - allocations = new uint256[](shareHolderIndex); - for (uint256 i = 0; i < shareHolderIndex; i++) { - uint256 share = balanceOfShareHolders[i] * - roleMultipliersOfShareHolders[i] * - hatsTimeFrameMultipliersOfShareHolders[i]; - totalAllocation += share; - allocations[i] = share; + for (uint i = 0; i < allocations.length; i++) { + totalAllocation += allocations[i]; } return (shareHolders, allocations, totalAllocation); diff --git a/pkgs/contract/test/BigBang.ts b/pkgs/contract/test/BigBang.ts index 326d81b..ee5ec7b 100644 --- a/pkgs/contract/test/BigBang.ts +++ b/pkgs/contract/test/BigBang.ts @@ -104,7 +104,7 @@ describe("BigBang", () => { it("should execute bigbang", async () => { // SplitsCreatorFactoryにBigBangアドレスをセット - SplitsCreatorFactory.write.setBigBang([BigBang.address]); + await SplitsCreatorFactory.write.setBigBang([BigBang.address]); const txHash = await BigBang.write.bigbang( [ diff --git a/pkgs/contract/test/IntegrationTest.ts b/pkgs/contract/test/IntegrationTest.ts index daf5da2..ae69785 100644 --- a/pkgs/contract/test/IntegrationTest.ts +++ b/pkgs/contract/test/IntegrationTest.ts @@ -278,7 +278,7 @@ describe("IntegrationTest", () => { address1.account?.address!, ]); - // address2のtokenの半分をaddress3に移動 + // address1のtokenの一部をaddress3に移動 await FractionToken.write.safeTransferFrom( [ address1.account?.address!, @@ -348,11 +348,12 @@ describe("IntegrationTest", () => { data: log.data, topics: log.topics, }); - if (decodedLog.eventName == "SplitsCreated") + if (decodedLog.eventName == "SplitsCreated") { splitAddress = decodedLog.args.split; - shareHolders = decodedLog.args.shareHolders; - allocations = decodedLog.args.allocations; - totalAllocation = decodedLog.args.totalAllocation; + shareHolders = decodedLog.args.shareHolders; + allocations = decodedLog.args.allocations; + totalAllocation = decodedLog.args.totalAllocation; + } } catch (error) { shareHolders = []; allocations = []; diff --git a/pkgs/contract/test/SplitsCreator.ts b/pkgs/contract/test/SplitsCreator.ts index a8a1fd9..a01e166 100644 --- a/pkgs/contract/test/SplitsCreator.ts +++ b/pkgs/contract/test/SplitsCreator.ts @@ -223,6 +223,7 @@ describe("CreateSplit", () => { let address1: WalletClient; let address2: WalletClient; let address3: WalletClient; + let address4: WalletClient; let bigBangAddress: WalletClient; let topHatId: bigint; @@ -233,7 +234,7 @@ describe("CreateSplit", () => { let address1WoreTime: bigint; let address2WoreTime: bigint; let address3WoreTime: bigint; - let address1_additional_woreTime: number = 500000; + let address1_additional_woreTime: number = 2592000; let publicClient: PublicClient; @@ -269,7 +270,7 @@ describe("CreateSplit", () => { const { SplitsCreator: _SplitsCreator } = await deploySplitsCreator(); SplitsCreator_IMPL = _SplitsCreator; - [address1, address2, address3, bigBangAddress] = + [address1, address2, address3, address4, bigBangAddress] = await viem.getWalletClients(); publicClient = await viem.getPublicClient(); @@ -310,7 +311,9 @@ describe("CreateSplit", () => { SplitsCreatorFactory = _SplitsCreatorFactory; - SplitsCreatorFactory.write.setBigBang([bigBangAddress.account?.address!]); + await SplitsCreatorFactory.write.setBigBang([ + bigBangAddress.account?.address!, + ]); let txHash = await SplitsCreatorFactory.write.createSplitCreatorDeterministic( @@ -477,14 +480,41 @@ describe("CreateSplit", () => { 0n, ]); - // let balance: bigint; + const tokenId = await FractionToken.read.getTokenId([ + hat1_id, + address1.account?.address!, + ]); + await FractionToken.write.safeTransferFrom( + [ + address1.account?.address!, + address4.account?.address!, + tokenId, + 3000n, + "0x", + ], + { + account: address1.account!, + } + ); + await FractionToken.write.safeTransferFrom( + [ + address1.account?.address!, + address3.account?.address!, + tokenId, + 1000n, + "0x", + ], + { + account: address1.account!, + } + ); const address1Balance = await FractionToken.read.balanceOf([ address1.account?.address!, address1.account?.address!, hat1_id, ]); - expect(address1Balance).to.equal(10000n); + expect(address1Balance).to.equal(6000n); // address2のbalance const address2Balance = await FractionToken.read.balanceOf([ @@ -555,7 +585,7 @@ describe("CreateSplit", () => { } } - expect(shareHolders.length).to.equal(3); + expect(shareHolders.length).to.equal(5); const address1Time = endWoreTime - address1WoreTime; const address2Time = endWoreTime - address2WoreTime; @@ -570,9 +600,20 @@ describe("CreateSplit", () => { address1.account?.address!, hat1_id, ]); - expect(address1Balance).to.equal(10000n); + expect(address1Balance).to.equal(6000n); + + const address3_address1Balance = await FractionToken.read.balanceOf([ + address3.account?.address!, + address1.account?.address!, + hat1_id, + ]); + + const address4Balance = await FractionToken.read.balanceOf([ + address4.account?.address!, + address1.account?.address!, + hat1_id, + ]); - // address2のbalance const address2Balance = await FractionToken.read.balanceOf([ address2.account?.address!, address2.account?.address!, @@ -580,7 +621,6 @@ describe("CreateSplit", () => { ]); expect(address2Balance).to.equal(10000n); - // address3のbalance const address3Balance = await FractionToken.read.balanceOf([ address3.account?.address!, address3.account?.address!, @@ -588,10 +628,22 @@ describe("CreateSplit", () => { ]); expect(address3Balance).to.equal(10000n); - expect(allocations.length).to.equal(3); - expect(allocations[0]).to.equal(address1Balance * 1n * sqrtAddress1Time); - expect(allocations[1]).to.equal(address2Balance * 1n * sqrtAddress2Time); - expect(allocations[2]).to.equal(address3Balance * 2n * sqrtAddress3Time); + expect(allocations.length).to.equal(5); + expect(allocations[0]).to.equal( + ((address1Balance * 1000000n) / 20000n) * 1n * sqrtAddress1Time + ); + expect(allocations[1]).to.equal( + ((address4Balance * 1000000n) / 20000n) * 1n * sqrtAddress1Time + ); + expect(allocations[2]).to.equal( + ((address3_address1Balance * 1000000n) / 20000n) * 1n * sqrtAddress1Time + ); + expect(allocations[3]).to.equal( + ((address2Balance * 1000000n) / 20000n) * 1n * sqrtAddress2Time + ); + expect(allocations[4]).to.equal( + ((address3Balance * 1000000n) / 10000n) * 2n * sqrtAddress3Time + ); await address1.sendTransaction({ account: address1.account!, @@ -691,7 +743,8 @@ describe("CreateSplit", () => { const previewResult = await SplitsCreator.read.preview([splitsInfo]); const shareHolders = previewResult[0]; - const percentages = previewResult[1]; + const allocations = previewResult[1]; + const totalAllocation = previewResult[2]; const endWoreTime = await publicClient .getBlock({ @@ -713,6 +766,12 @@ describe("CreateSplit", () => { hat1_id, ]); + const address4Balance = await FractionToken.read.balanceOf([ + address4.account?.address!, + address1.account?.address!, + hat1_id, + ]); + const address2Balance = await FractionToken.read.balanceOf([ address2.account?.address!, address2.account?.address!, @@ -725,23 +784,28 @@ describe("CreateSplit", () => { hat2_id, ]); - const allocation0 = address1Balance * 1n * sqrtAddress1Time; - const allocation1 = address2Balance * 1n * sqrtAddress2Time; - const allocation2 = address3Balance * 2n * sqrtAddress3Time; - - const expectedAllocations = [allocation0, allocation1, allocation2]; - - const totalAllocation = allocation0 + allocation1 + allocation2; - - const expectedPercentages = expectedAllocations.map( - (allocation) => - (allocation * 1_000_000_000_000_000_000n) / totalAllocation - ); + const allocation0 = + ((address1Balance * 1000000n) / 20000n) * 1n * sqrtAddress1Time; + const allocation1 = + ((address4Balance * 1000000n) / 20000n) * 1n * sqrtAddress1Time; + const allocation2 = + ((address2Balance * 1000000n) / 20000n) * 1n * sqrtAddress2Time; + const allocation3 = + ((address3Balance * 1000000n) / 10000n) * 2n * sqrtAddress3Time; + + const expectedAllocations = [ + allocation0, + allocation1, + allocation2, + allocation3, + ]; - expect(shareHolders.length).to.equal(3); + expect(shareHolders.length).to.equal(5); const expectedShareHolders = [ address1.account?.address!, + address4.account?.address!, + address3.account?.address!, address2.account?.address!, address3.account?.address!, ]; @@ -756,11 +820,18 @@ describe("CreateSplit", () => { expect(shareHolders[2].toLowerCase()).to.equal( expectedShareHolders[2].toLowerCase() ); + expect(shareHolders[3].toLowerCase()).to.equal( + expectedShareHolders[3].toLowerCase() + ); + expect(shareHolders[4].toLowerCase()).to.equal( + expectedShareHolders[4].toLowerCase() + ); - expect(percentages.length).to.equal(3); + expect(allocations.length).to.equal(5); - expect(percentages[0]).to.equal(expectedPercentages[0]); - expect(percentages[1]).to.equal(expectedPercentages[1]); - expect(percentages[2]).to.equal(expectedPercentages[2]); + expect(allocations[0]).to.equal(expectedAllocations[0]); + expect(allocations[1]).to.equal(expectedAllocations[1]); + expect(allocations[3]).to.equal(expectedAllocations[2]); + expect(allocations[4]).to.equal(expectedAllocations[3]); }); }); diff --git a/pkgs/frontend/abi/splits.ts b/pkgs/frontend/abi/splits.ts index 9fead4d..7a16c02 100644 --- a/pkgs/frontend/abi/splits.ts +++ b/pkgs/frontend/abi/splits.ts @@ -8,6 +8,24 @@ export const SPLITS_CREATOR_ABI = [ name: "split", type: "address", }, + { + indexed: false, + internalType: "address[]", + name: "shareHolders", + type: "address[]", + }, + { + indexed: false, + internalType: "uint256[]", + name: "allocations", + type: "uint256[]", + }, + { + indexed: false, + internalType: "uint256", + name: "totalAllocation", + type: "uint256", + }, ], name: "SplitsCreated", type: "event", @@ -27,10 +45,10 @@ export const SPLITS_CREATOR_ABI = [ }, { inputs: [], - name: "HATS_TIME_FRAME_MODULE", + name: "HATS", outputs: [ { - internalType: "contract IHatsTimeFrameModule", + internalType: "contract IHats", name: "", type: "address", }, @@ -40,10 +58,10 @@ export const SPLITS_CREATOR_ABI = [ }, { inputs: [], - name: "SPLIT_FACTORY_V2", + name: "HATS_TIME_FRAME_MODULE", outputs: [ { - internalType: "contract ISplitFactoryV2", + internalType: "contract IHatsTimeFrameModule", name: "", type: "address", }, @@ -53,10 +71,10 @@ export const SPLITS_CREATOR_ABI = [ }, { inputs: [], - name: "TRUSTED_FORWARDER", + name: "SPLIT_FACTORY_V2", outputs: [ { - internalType: "address", + internalType: "contract ISplitFactoryV2", name: "", type: "address", }, @@ -105,4 +123,55 @@ export const SPLITS_CREATOR_ABI = [ stateMutability: "nonpayable", type: "function", }, + { + inputs: [ + { + components: [ + { + internalType: "uint256", + name: "hatId", + type: "uint256", + }, + { + internalType: "uint256", + name: "multiplierBottom", + type: "uint256", + }, + { + internalType: "uint256", + name: "multiplierTop", + type: "uint256", + }, + { + internalType: "address[]", + name: "wearers", + type: "address[]", + }, + ], + internalType: "struct ISplitsCreator.SplitsInfo[]", + name: "_splitsInfo", + type: "tuple[]", + }, + ], + name: "preview", + outputs: [ + { + internalType: "address[]", + name: "shareHolders", + type: "address[]", + }, + { + internalType: "uint256[]", + name: "allocations", + type: "uint256[]", + }, + { + internalType: "uint256", + name: "totalAllocation", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, ] as const; diff --git a/pkgs/frontend/app/routes/$treeId_.splitter.new.tsx b/pkgs/frontend/app/routes/$treeId_.splitter.new.tsx index 060ed62..a2bd49b 100644 --- a/pkgs/frontend/app/routes/$treeId_.splitter.new.tsx +++ b/pkgs/frontend/app/routes/$treeId_.splitter.new.tsx @@ -5,12 +5,26 @@ import { HStack, List, Separator, + Stack, Text, + VStack, } from "@chakra-ui/react"; +import { Hat, Wearer } from "@hatsprotocol/sdk-v1-subgraph"; import { useParams } from "@remix-run/react"; -import { useAssignableHats } from "hooks/useHats"; -import { FC } from "react"; +import { useNamesByAddresses } from "hooks/useENS"; +import { useAssignableHats, useHats } from "hooks/useHats"; +import { useSplitsCreator } from "hooks/useSplitsCreator"; +import { + ChangeEvent, + FC, + useCallback, + useEffect, + useMemo, + useState, +} from "react"; import { HatsDetailSchama } from "types/hats"; +import { ipfs2https, ipfs2httpsJson } from "utils/ipfs"; +import { Address } from "viem"; import { BasicButton } from "~/components/BasicButton"; import { CommonDialog } from "~/components/common/CommonDialog"; import { CommonInput } from "~/components/common/CommonInput"; @@ -20,17 +34,75 @@ import { UserIcon } from "~/components/icon/UserIcon"; import { PageHeader } from "~/components/PageHeader"; import { Checkbox } from "~/components/ui/checkbox"; import { Field } from "~/components/ui/field"; +import { + FieldArrayWithId, + useFieldArray, + UseFieldArrayUpdate, + useForm, +} from "react-hook-form"; interface RoleItemProps { + update: UseFieldArrayUpdate; + fieldIndex: number; detail?: HatsDetailSchama; imageUri?: string; + field: FieldArrayWithId; } -const RoleItem: FC = ({ detail, imageUri }) => { +const RoleItem: FC = ({ + detail, + imageUri, + update, + fieldIndex, + field, +}) => { + const { getWearersInfo } = useHats(); + + const [wearersAddress, setWearersAddress] = useState([]); + + useEffect(() => { + const fetch = async () => { + const res = await getWearersInfo({ hatId: field.hatId }); + if (!res) return; + setWearersAddress(res.map((w) => w.id)); + }; + fetch(); + }, [field.hatId, getWearersInfo]); + + const { names } = useNamesByAddresses(wearersAddress); + + const handleOnCheck = (address: Address) => { + if (!address) return; + const array = field.wearers.includes(address) + ? field.wearers.filter((a) => a !== address) + : [...field.wearers, address]; + update(fieldIndex, { + ...field, + wearers: array, + }); + }; + + const handleUpdateMultiplier = (e: ChangeEvent) => { + const value = Number(e.target.value); + update(fieldIndex, { + ...field, + multiplier: value, + }); + }; + return ( - + { + update(fieldIndex, { + ...field, + active: !field.active, + }); + }} + /> {detail?.data.name} @@ -38,8 +110,8 @@ const RoleItem: FC = ({ detail, imageUri }) => { 分配係数 {}} + value={field.multiplier} + onChange={handleUpdateMultiplier} placeholder="例: 1, 1.5 10" textAlign="center" w="80px" @@ -57,16 +129,35 @@ const RoleItem: FC = ({ detail, imageUri }) => { 分配対象にするメンバーと役割を選択 - - - - - User name - 0x123 - - + + {names + .filter((name) => name.length > 0) + .map((name, index) => ( + + + handleOnCheck( + name[0]?.address.toLowerCase() as Address + ) + } + /> + + + {name[0]?.name} + {name[0]?.address} + + + ))} + - + {/* @@ -90,7 +181,7 @@ const RoleItem: FC = ({ detail, imageUri }) => { - + */} @@ -99,36 +190,156 @@ const RoleItem: FC = ({ detail, imageUri }) => { ); }; +interface FormData { + roles: RoleInput[]; +} + +interface RoleInput { + hat: Pick; + hatId: Address; + active: boolean; + multiplier: number; + wearers: Address[]; +} + const SplitterNew: FC = () => { const { treeId } = useParams(); const hats = useAssignableHats(Number(treeId)); - const baseHats = hats.filter((h) => Number(h.levelAtLocalTree) == 2); + const baseHats = useMemo(() => { + return hats.filter( + (h) => Number(h.levelAtLocalTree) == 2 && h.wearers?.length + ); + }, [hats]); + + const { createSplits, previewSplits } = useSplitsCreator(treeId!); + + const { control, getValues } = useForm(); + const { fields, insert, update } = useFieldArray({ + control, + name: "roles", + }); + + useEffect(() => { + const fetch = async () => { + if (fields.length == 0) { + for (let index = 0; index < baseHats.length; index++) { + const hat = baseHats[index]; + insert(index, { + hat, + hatId: hat.id, + active: true, + multiplier: 1, + wearers: hat.wearers?.map((w) => w.id) || [], + }); + } + } + }; + fetch(); + }, [baseHats, fields]); + + const [preview, setPreview] = + useState<{ address: Address; ratio: string }[]>(); + + const handlePreview = async () => { + const data = getValues(); + + const params = data.roles.map((role) => ({ + hatId: BigInt(role.hatId), + multiplierBottom: role.multiplier + ? BigInt(String(role.multiplier).split(".")[1].length * 10) + : BigInt(1), + multiplierTop: role.multiplier + ? BigInt( + role.multiplier * String(role.multiplier).split(".")[1].length * 10 + ) + : BigInt(1), + wearers: role.wearers, + })); + const res = await previewSplits(params); + + const _preview = []; + const sumOfScore = res[1].reduce((acc, cur) => acc + Number(cur), 0); + for (let index = 0; index < res[0].length; index++) { + const address = res[0][index]; + const score = res[1][index]; + const ratio = ((Number(score) / sumOfScore) * 100).toFixed(2); + _preview.push({ address, ratio }); + } + setPreview(_preview); + }; + + const handleCreateSplitter = async () => { + const data = getValues(); + const params = data.roles.map((role) => ({ + hatId: BigInt(role.hatId), + multiplierBottom: role.multiplier + ? BigInt(String(role.multiplier).split(".")[1].length * 10) + : BigInt(1), + multiplierTop: role.multiplier + ? BigInt( + role.multiplier * String(role.multiplier).split(".")[1].length * 10 + ) + : BigInt(1), + wearers: role.wearers, + })); + const txHash = await createSplits({ + args: params, + }); + console.log(txHash); + }; return ( <> - - - - {}} placeholder="名前" /> - - - - 分配設定 - - - {baseHats.map((h, index) => ( - - - - ))} - - プレビュー + { + setPreview(undefined); + } + : undefined + } + /> + + {preview ? ( + <> + + {preview.map((item, index) => ( + + + {item.address} + {item.ratio}% + + + ))} + + 作成 + + ) : ( + <> + + {}} placeholder="名前" /> + + + + 分配設定 + + + {fields.map((field, index) => ( + + + + ))} + + プレビュー + + )} ); }; diff --git a/pkgs/frontend/hooks/useENS.ts b/pkgs/frontend/hooks/useENS.ts index 7497294..40d21d7 100644 --- a/pkgs/frontend/hooks/useENS.ts +++ b/pkgs/frontend/hooks/useENS.ts @@ -23,7 +23,7 @@ export const useNamesByAddresses = (addresses?: string[]) => { const [names, setNames] = useState([]); useEffect(() => { - if (!addresses) return; + if (!addresses || addresses.length == 0) return; fetchNames(addresses); }, [addresses]); diff --git a/pkgs/frontend/hooks/useHats.ts b/pkgs/frontend/hooks/useHats.ts index a409a32..0d46c02 100644 --- a/pkgs/frontend/hooks/useHats.ts +++ b/pkgs/frontend/hooks/useHats.ts @@ -42,7 +42,6 @@ export const useTreeInfo = (treeId: number) => { const tree = await getTreeInfo({ treeId: treeId, }); - if (!tree) return; setTreeInfo(tree); @@ -68,47 +67,42 @@ export const useHats = () => { * @param chainId * @param treeId */ - const getTreeInfo = useCallback( - async (params: { treeId: number }) => { - if (!wallet) return; + const getTreeInfo = useCallback(async (params: { treeId: number }) => { + setIsLoading(true); - setIsLoading(true); - - try { - const tree = await hatsSubgraphClient.getTree({ - chainId: currentChain.id, - treeId: params.treeId, - props: { - hats: { - props: { - prettyId: true, - status: true, - createdAt: true, - details: true, - maxSupply: true, - eligibility: true, - imageUri: true, - toggle: true, - levelAtLocalTree: true, - currentSupply: true, - wearers: { - props: {}, - }, + try { + const tree = await hatsSubgraphClient.getTree({ + chainId: currentChain.id, + treeId: params.treeId, + props: { + hats: { + props: { + prettyId: true, + status: true, + createdAt: true, + details: true, + maxSupply: true, + eligibility: true, + imageUri: true, + toggle: true, + levelAtLocalTree: true, + currentSupply: true, + wearers: { + props: {}, }, }, }, - }); + }, + }); - return tree; - } catch (error) { - console.error("error occured when fetching treeInfo:", error); - return null; - } finally { - setIsLoading(false); - } - }, - [wallet] - ); + return tree; + } catch (error) { + console.error("error occured when fetching treeInfo:", error); + return null; + } finally { + setIsLoading(false); + } + }, []); /** * 特定のウォレットアドレスが着用している全てのHats情報を取得するコールバック関数 diff --git a/pkgs/frontend/hooks/useSplitsCreator.ts b/pkgs/frontend/hooks/useSplitsCreator.ts index babfe44..dfe309d 100644 --- a/pkgs/frontend/hooks/useSplitsCreator.ts +++ b/pkgs/frontend/hooks/useSplitsCreator.ts @@ -1,41 +1,53 @@ import { SPLITS_CREATOR_ABI } from "abi/splits"; -import { useCallback, useState } from "react"; +import { useCallback, useMemo, useState } from "react"; import { AbiItemArgs, Address, encodeFunctionData } from "viem"; -import { useSmartAccountClient } from "./useWallet"; +import { useActiveWallet, useSmartAccountClient } from "./useWallet"; import { publicClient } from "./useViem"; +import { useGetWorkspace } from "./useWorkspace"; /** * Splits creator 用 React hooks */ -export const useSplitsCreator = () => { - const smartAccountClient = useSmartAccountClient(); +export const useSplitsCreator = (treeId: string) => { + const { wallet } = useActiveWallet(); const [isLoading, setIsLoading] = useState(false); + const { data } = useGetWorkspace(treeId); + const splitsCreatorAddress = useMemo(() => { + return data?.workspace?.splitCreator as Address; + }, [data?.workspace?.splitCreator]); + + const previewSplits = async ( + args: AbiItemArgs[0] + ) => { + const res = await publicClient.readContract({ + address: splitsCreatorAddress, + abi: SPLITS_CREATOR_ABI, + functionName: "preview", + args: [args], + }); + + return res; + }; + /** * Splitsを作成するコールバック関数 */ const createSplits = useCallback( async (params: { - splitsCreatorAddress: Address; args: AbiItemArgs[0]; }) => { - if (!smartAccountClient) return; + if (!wallet || !splitsCreatorAddress) return; setIsLoading(true); try { - const txHash = await smartAccountClient.sendTransaction({ - calls: [ - { - to: params.splitsCreatorAddress, - data: encodeFunctionData({ - abi: SPLITS_CREATOR_ABI, - functionName: "create", - args: [params.args], - }), - }, - ], + const txHash = await wallet.writeContract({ + address: splitsCreatorAddress, + abi: SPLITS_CREATOR_ABI, + functionName: "create", + args: [params.args], }); const receipt = await publicClient.waitForTransactionReceipt({ @@ -51,11 +63,12 @@ export const useSplitsCreator = () => { setIsLoading(false); } }, - [smartAccountClient] + [splitsCreatorAddress, wallet] ); return { isLoading, createSplits, + previewSplits, }; }; diff --git a/pkgs/frontend/package.json b/pkgs/frontend/package.json index 68af49e..3f35230 100644 --- a/pkgs/frontend/package.json +++ b/pkgs/frontend/package.json @@ -35,6 +35,7 @@ "permissionless": "^0.2.20", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-hook-form": "^7.54.2", "react-icons": "^5.4.0", "viem": "^2.21.51" }, diff --git a/yarn.lock b/yarn.lock index 414649b..7ecc129 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16799,6 +16799,11 @@ react-helmet-async@^1.3.0: react-fast-compare "^3.2.0" shallowequal "^1.1.0" +react-hook-form@^7.54.2: + version "7.54.2" + resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.54.2.tgz#8c26ed54c71628dff57ccd3c074b1dd377cfb211" + integrity sha512-eHpAUgUjWbZocoQYUHposymRb4ZP6d0uwUnooL2uOybA9/3tPUvoAKqEWK1WaSiTxxOfTpffNZP7QwlnM3/gEg== + react-icons@^5.4.0: version "5.4.0" resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-5.4.0.tgz#443000f6e5123ee1b21ea8c0a716f6e7797f7416"