@towns-protocol/contracts 0.0.374 → 0.0.376

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@towns-protocol/contracts",
3
- "version": "0.0.374",
3
+ "version": "0.0.376",
4
4
  "packageManager": "yarn@3.8.0",
5
5
  "scripts": {
6
6
  "build-types": "bash scripts/build-contract-types.sh",
@@ -35,7 +35,7 @@
35
35
  "@layerzerolabs/oapp-evm": "^0.3.2",
36
36
  "@openzeppelin/merkle-tree": "^1.0.8",
37
37
  "@prb/test": "^0.6.4",
38
- "@towns-protocol/prettier-config": "^0.0.374",
38
+ "@towns-protocol/prettier-config": "^0.0.376",
39
39
  "@typechain/ethers-v5": "^11.1.2",
40
40
  "@wagmi/cli": "^2.2.0",
41
41
  "forge-std": "github:foundry-rs/forge-std#v1.10.0",
@@ -57,5 +57,5 @@
57
57
  "publishConfig": {
58
58
  "access": "public"
59
59
  },
60
- "gitHead": "b6f28ce51336d969161de5c5aa0aa012f03a182a"
60
+ "gitHead": "500000c0d1b16a866df765a75c7972b5d1691693"
61
61
  }
@@ -13,6 +13,8 @@ import {LibString} from "solady/utils/LibString.sol";
13
13
  import {DeployMetadata} from "../facets/DeployMetadata.s.sol";
14
14
  import {DeployAppRegistryFacet} from "../facets/DeployAppRegistryFacet.s.sol";
15
15
  import {DeployUpgradeableBeacon} from "../facets/DeployUpgradeableBeacon.s.sol";
16
+ import {DeployAppInstallerFacet} from "../facets/DeployAppInstallerFacet.s.sol";
17
+ import {DeploySpaceFactory} from "../diamonds/DeploySpaceFactory.s.sol";
16
18
 
17
19
  // contracts
18
20
  import {Diamond} from "@towns-protocol/diamond/src/Diamond.sol";
@@ -26,29 +28,19 @@ import {Deployer} from "../../common/Deployer.s.sol";
26
28
  contract DeployAppRegistry is IDiamondInitHelper, DiamondHelper, Deployer {
27
29
  using LibString for string;
28
30
 
29
- address private SPACE_FACTORY;
30
-
31
31
  DeployFacet private facetHelper = new DeployFacet();
32
+ DeploySpaceFactory private deploySpaceFactory = new DeploySpaceFactory();
32
33
 
33
34
  string internal constant APP_REGISTRY_SCHEMA = "address app, address client";
35
+ address internal spaceFactory;
34
36
 
35
37
  function versionName() public pure override returns (string memory) {
36
38
  return "appRegistry";
37
39
  }
38
40
 
39
- function setSpaceFactory(address factory) public {
40
- SPACE_FACTORY = factory;
41
- }
42
-
43
- function getSpaceFactory() public returns (address) {
44
- if (SPACE_FACTORY != address(0)) {
45
- return SPACE_FACTORY;
46
- }
47
-
48
- return getDeployment("spaceFactory");
49
- }
50
-
51
41
  function addImmutableCuts(address deployer) internal {
42
+ spaceFactory = deploySpaceFactory.deploy(deployer);
43
+
52
44
  // Queue up all core facets for batch deployment
53
45
  facetHelper.add("DiamondCutFacet");
54
46
  facetHelper.add("DiamondLoupeFacet");
@@ -94,6 +86,7 @@ contract DeployAppRegistry is IDiamondInitHelper, DiamondHelper, Deployer {
94
86
  facetHelper.add("MetadataFacet");
95
87
  facetHelper.add("UpgradeableBeaconFacet");
96
88
  facetHelper.add("AppRegistryFacet");
89
+ facetHelper.add("AppInstallerFacet");
97
90
  facetHelper.add("SimpleApp");
98
91
 
99
92
  facetHelper.deployBatch(deployer);
@@ -118,7 +111,14 @@ contract DeployAppRegistry is IDiamondInitHelper, DiamondHelper, Deployer {
118
111
  addFacet(
119
112
  makeCut(facet, FacetCutAction.Add, DeployAppRegistryFacet.selectors()),
120
113
  facet,
121
- DeployAppRegistryFacet.makeInitData(getSpaceFactory(), APP_REGISTRY_SCHEMA, address(0))
114
+ DeployAppRegistryFacet.makeInitData(spaceFactory, APP_REGISTRY_SCHEMA, address(0))
115
+ );
116
+
117
+ facet = facetHelper.getDeployedAddress("AppInstallerFacet");
118
+ addFacet(
119
+ makeCut(facet, FacetCutAction.Add, DeployAppInstallerFacet.selectors()),
120
+ facet,
121
+ DeployAppInstallerFacet.makeInitData()
122
122
  );
123
123
 
124
124
  address multiInit = facetHelper.getDeployedAddress("MultiInit");
@@ -150,6 +150,9 @@ contract DeployAppRegistry is IDiamondInitHelper, DiamondHelper, Deployer {
150
150
  if (facetName.eq("AppRegistryFacet")) {
151
151
  addCut(makeCut(facet, FacetCutAction.Add, DeployAppRegistryFacet.selectors()));
152
152
  }
153
+ if (facetName.eq("AppInstallerFacet")) {
154
+ addCut(makeCut(facet, FacetCutAction.Add, DeployAppInstallerFacet.selectors()));
155
+ }
153
156
  }
154
157
 
155
158
  return baseFacets();
@@ -0,0 +1,48 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.23;
3
+
4
+ //interfaces
5
+ import {IDiamond} from "@towns-protocol/diamond/src/IDiamond.sol";
6
+
7
+ //libraries
8
+
9
+ //contracts
10
+ import {LibDeploy} from "@towns-protocol/diamond/src/utils/LibDeploy.sol";
11
+ import {AppInstallerFacet} from "src/apps/facets/installer/AppInstallerFacet.sol";
12
+ import {DynamicArrayLib} from "solady/utils/DynamicArrayLib.sol";
13
+
14
+ library DeployAppInstallerFacet {
15
+ using DynamicArrayLib for DynamicArrayLib.DynamicArray;
16
+
17
+ function selectors() internal pure returns (bytes4[] memory res) {
18
+ DynamicArrayLib.DynamicArray memory arr = DynamicArrayLib.p().reserve(4);
19
+ arr.p(AppInstallerFacet.installApp.selector);
20
+ arr.p(AppInstallerFacet.uninstallApp.selector);
21
+ arr.p(AppInstallerFacet.updateApp.selector);
22
+ arr.p(AppInstallerFacet.renewApp.selector);
23
+ bytes32[] memory selectors_ = arr.asBytes32Array();
24
+ assembly ("memory-safe") {
25
+ res := selectors_
26
+ }
27
+ }
28
+
29
+ function makeCut(
30
+ address facetAddress,
31
+ IDiamond.FacetCutAction action
32
+ ) internal pure returns (IDiamond.FacetCut memory) {
33
+ return
34
+ IDiamond.FacetCut({
35
+ action: action,
36
+ facetAddress: facetAddress,
37
+ functionSelectors: selectors()
38
+ });
39
+ }
40
+
41
+ function makeInitData() internal pure returns (bytes memory) {
42
+ return abi.encodeCall(AppInstallerFacet.__AppInstaller_init, ());
43
+ }
44
+
45
+ function deploy() internal returns (address) {
46
+ return LibDeploy.deployCode("AppInstallerFacet.sol", "");
47
+ }
48
+ }
@@ -16,7 +16,7 @@ library DeployAppRegistryFacet {
16
16
  using DynamicArrayLib for DynamicArrayLib.DynamicArray;
17
17
 
18
18
  function selectors() internal pure returns (bytes4[] memory res) {
19
- DynamicArrayLib.DynamicArray memory arr = DynamicArrayLib.p().reserve(18);
19
+ DynamicArrayLib.DynamicArray memory arr = DynamicArrayLib.p().reserve(14);
20
20
  arr.p(AppRegistryFacet.getAppSchema.selector);
21
21
  arr.p(AppRegistryFacet.getAppSchemaId.selector);
22
22
  arr.p(AppRegistryFacet.getAppById.selector);
@@ -25,16 +25,12 @@ library DeployAppRegistryFacet {
25
25
  arr.p(AppRegistryFacet.removeApp.selector);
26
26
  arr.p(AppRegistryFacet.createApp.selector);
27
27
  arr.p(AppRegistryFacet.upgradeApp.selector);
28
- arr.p(AppRegistryFacet.installApp.selector);
29
- arr.p(AppRegistryFacet.uninstallApp.selector);
30
- arr.p(AppRegistryFacet.updateApp.selector);
31
28
  arr.p(AppRegistryFacet.getAppPrice.selector);
32
29
  arr.p(AppRegistryFacet.getAppDuration.selector);
33
30
  arr.p(AppRegistryFacet.adminRegisterAppSchema.selector);
34
31
  arr.p(AppRegistryFacet.adminBanApp.selector);
35
32
  arr.p(AppRegistryFacet.isAppBanned.selector);
36
33
  arr.p(AppRegistryFacet.getAppByClient.selector);
37
- arr.p(AppRegistryFacet.renewApp.selector);
38
34
  bytes32[] memory selectors_ = arr.asBytes32Array();
39
35
  assembly ("memory-safe") {
40
36
  res := selectors_
@@ -7,15 +7,26 @@ import {ITipping} from "src/spaces/facets/tipping/ITipping.sol";
7
7
 
8
8
  // libraries
9
9
  import {LibDeploy} from "@towns-protocol/diamond/src/utils/LibDeploy.sol";
10
+ import {DynamicArrayLib} from "solady/utils/DynamicArrayLib.sol";
10
11
 
11
12
  library DeployTipping {
13
+ using DynamicArrayLib for DynamicArrayLib.DynamicArray;
14
+
12
15
  function selectors() internal pure returns (bytes4[] memory res) {
13
- res = new bytes4[](5);
14
- res[0] = ITipping.tip.selector;
15
- res[1] = ITipping.tipsByCurrencyAndTokenId.selector;
16
- res[2] = ITipping.tippingCurrencies.selector;
17
- res[3] = ITipping.totalTipsByCurrency.selector;
18
- res[4] = ITipping.tipAmountByCurrency.selector;
16
+ DynamicArrayLib.DynamicArray memory arr = DynamicArrayLib.p().reserve(8);
17
+ arr.p(ITipping.sendTip.selector);
18
+ arr.p(ITipping.tip.selector);
19
+ arr.p(ITipping.tipsByWalletAndCurrency.selector);
20
+ arr.p(ITipping.tipCountByWalletAndCurrency.selector);
21
+ arr.p(ITipping.tipsByCurrencyAndTokenId.selector);
22
+ arr.p(ITipping.tippingCurrencies.selector);
23
+ arr.p(ITipping.totalTipsByCurrency.selector);
24
+ arr.p(ITipping.tipAmountByCurrency.selector);
25
+
26
+ bytes32[] memory selectors_ = arr.asBytes32Array();
27
+ assembly ("memory-safe") {
28
+ res := selectors_
29
+ }
19
30
  }
20
31
 
21
32
  function makeCut(
@@ -0,0 +1,66 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.29;
3
+
4
+ // interfaces
5
+ import {ITownsApp} from "../../ITownsApp.sol";
6
+ import {IAppAccount} from "../../../spaces/facets/account/IAppAccount.sol";
7
+ import {IAppInstaller} from "./IAppInstaller.sol";
8
+
9
+ // libraries
10
+
11
+ // contracts
12
+ import {Facet} from "@towns-protocol/diamond/src/facets/Facet.sol";
13
+ import {AppRegistryBase} from "../registry/AppRegistryBase.sol";
14
+ import {ReentrancyGuardTransient} from "solady/utils/ReentrancyGuardTransient.sol";
15
+
16
+ /// @title AppInstallerFacet
17
+ /// @author Towns Protocol
18
+ /// @notice Facet for installing apps to spaces
19
+ contract AppInstallerFacet is IAppInstaller, AppRegistryBase, ReentrancyGuardTransient, Facet {
20
+ function __AppInstaller_init() external onlyInitializing {
21
+ _addInterface(type(IAppInstaller).interfaceId);
22
+ }
23
+
24
+ /// @notice Install an app
25
+ /// @param app The app address to install
26
+ /// @param space The space to install the app to
27
+ /// @param data The data to pass to the app's onInstall function
28
+ function installApp(
29
+ ITownsApp app,
30
+ IAppAccount space,
31
+ bytes calldata data
32
+ ) external payable nonReentrant onlyAllowed(space) {
33
+ _installApp(address(app), address(space), data);
34
+ }
35
+
36
+ /// @notice Uninstall an app
37
+ /// @param app The app address to uninstall
38
+ /// @param space The space to uninstall the app from
39
+ /// @param data The data to pass to the app's onUninstall function
40
+ function uninstallApp(
41
+ ITownsApp app,
42
+ IAppAccount space,
43
+ bytes calldata data
44
+ ) external nonReentrant onlyAllowed(space) {
45
+ _uninstallApp(address(app), address(space), data);
46
+ }
47
+
48
+ /// @notice Update an app to the latest version
49
+ /// @param app The app address to update
50
+ /// @param space The space to update the app to
51
+ function updateApp(ITownsApp app, IAppAccount space) external nonReentrant onlyAllowed(space) {
52
+ _updateApp(address(app), address(space));
53
+ }
54
+
55
+ /// @notice Renew an app
56
+ /// @param app The app address to renew
57
+ /// @param space The space to renew the app for
58
+ /// @param data The data to pass to the app's onRenewApp function
59
+ function renewApp(
60
+ ITownsApp app,
61
+ IAppAccount space,
62
+ bytes calldata data
63
+ ) external payable nonReentrant onlyAllowed(space) {
64
+ _renewApp(address(app), address(space), data);
65
+ }
66
+ }
@@ -0,0 +1,35 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.29;
3
+
4
+ // interfaces
5
+ import {ITownsApp} from "../../ITownsApp.sol";
6
+ import {IAppAccount} from "../../../spaces/facets/account/IAppAccount.sol";
7
+
8
+ // libraries
9
+
10
+ // contracts
11
+
12
+ interface IAppInstaller {
13
+ /// @notice Install an app
14
+ /// @param app The app address to install
15
+ /// @param account The account to install the app to
16
+ /// @param data The data to pass to the app's onInstall function
17
+ function installApp(ITownsApp app, IAppAccount account, bytes calldata data) external payable;
18
+
19
+ /// @notice Uninstall an app
20
+ /// @param app The app address to uninstall
21
+ /// @param account The account to uninstall the app from
22
+ /// @param data The data to pass to the app's onUninstall function
23
+ function uninstallApp(ITownsApp app, IAppAccount account, bytes calldata data) external;
24
+
25
+ /// @notice Update an app to the latest version
26
+ /// @param app The app address to update
27
+ /// @param account The account to update the app to
28
+ function updateApp(ITownsApp app, IAppAccount account) external;
29
+
30
+ /// @notice Renew an app
31
+ /// @param app The app address to renew
32
+ /// @param account The account to renew the app for
33
+ /// @param data The data to pass to the app's onRenewApp function
34
+ function renewApp(ITownsApp app, IAppAccount account, bytes calldata data) external payable;
35
+ }
@@ -35,6 +35,11 @@ abstract contract AppRegistryBase is IAppRegistryBase, SchemaBase, AttestationBa
35
35
 
36
36
  uint48 private constant MAX_DURATION = 365 days;
37
37
 
38
+ modifier onlyAllowed(IAppAccount account) {
39
+ if (IERC173(address(account)).owner() != msg.sender) NotAllowed.selector.revertWith();
40
+ _;
41
+ }
42
+
38
43
  function __AppRegistry_init_unchained(
39
44
  address spaceFactory,
40
45
  string calldata schema,
@@ -321,10 +326,6 @@ abstract contract AppRegistryBase is IAppRegistryBase, SchemaBase, AttestationBa
321
326
  emit AppRenewed(app, account, appId);
322
327
  }
323
328
 
324
- function _onlyAllowed(address account) internal view {
325
- if (IERC173(account).owner() != msg.sender) NotAllowed.selector.revertWith();
326
- }
327
-
328
329
  function _getProtocolFee(uint256 installPrice) internal view returns (uint256) {
329
330
  IPlatformRequirements platform = _getPlatformRequirements();
330
331
  uint256 baseFee = platform.getMembershipFee();
@@ -16,6 +16,9 @@ import {Facet} from "@towns-protocol/diamond/src/facets/Facet.sol";
16
16
  import {ReentrancyGuard} from "solady/utils/ReentrancyGuard.sol";
17
17
  import {OwnableBase} from "@towns-protocol/diamond/src/facets/ownable/OwnableBase.sol";
18
18
 
19
+ /// @title AppRegistryFacet
20
+ /// @author Towns Protocol
21
+ /// @notice Facet for managing app registry
19
22
  contract AppRegistryFacet is IAppRegistry, AppRegistryBase, OwnableBase, ReentrancyGuard, Facet {
20
23
  function __AppRegistry_init(
21
24
  address spaceFactory,
@@ -95,53 +98,6 @@ contract AppRegistryFacet is IAppRegistry, AppRegistryBase, OwnableBase, Reentra
95
98
  /* Space Functions */
96
99
  /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
97
100
 
98
- /// @notice Install an app
99
- /// @param app The app address to install
100
- /// @param space The space to install the app to
101
- /// @param data The data to pass to the app's onInstall function
102
- function installApp(
103
- ITownsApp app,
104
- IAppAccount space,
105
- bytes calldata data
106
- ) external payable nonReentrant {
107
- _onlyAllowed(address(space));
108
- return _installApp(address(app), address(space), data);
109
- }
110
-
111
- /// @notice Uninstall an app
112
- /// @param app The app address to uninstall
113
- /// @param space The space to uninstall the app from
114
- /// @param data The data to pass to the app's onUninstall function
115
- function uninstallApp(
116
- ITownsApp app,
117
- IAppAccount space,
118
- bytes calldata data
119
- ) external nonReentrant {
120
- _onlyAllowed(address(space));
121
- _uninstallApp(address(app), address(space), data);
122
- }
123
-
124
- /// @notice Update an app to the latest version
125
- /// @param app The app address to update
126
- /// @param space The space to update the app to
127
- function updateApp(ITownsApp app, IAppAccount space) external nonReentrant {
128
- _onlyAllowed(address(space));
129
- _updateApp(address(app), address(space));
130
- }
131
-
132
- /// @notice Renew an app
133
- /// @param app The app address to renew
134
- /// @param account The account to renew the app for
135
- /// @param data The data to pass to the app's onRenewApp function
136
- function renewApp(
137
- ITownsApp app,
138
- IAppAccount account,
139
- bytes calldata data
140
- ) external payable nonReentrant {
141
- _onlyAllowed(address(account));
142
- _renewApp(address(app), address(account), data);
143
- }
144
-
145
101
  /// @notice Get the schema structure used for registering modules
146
102
  /// @return The schema structure
147
103
  function getAppSchema() external view returns (string memory) {
@@ -140,29 +140,6 @@ interface IAppRegistry is IAppRegistryBase {
140
140
  /// @param appId The app ID to remove
141
141
  function removeApp(bytes32 appId) external;
142
142
 
143
- /// @notice Renew an app
144
- /// @param app The app address to renew
145
- /// @param account The account to renew the app for
146
- /// @param data The data to pass to the app's onRenewApp function
147
- function renewApp(ITownsApp app, IAppAccount account, bytes calldata data) external payable;
148
-
149
- /// @notice Install an app
150
- /// @param app The app address to install
151
- /// @param account The account to install the app to
152
- /// @param data The data to pass to the app's onInstall function
153
- function installApp(ITownsApp app, IAppAccount account, bytes calldata data) external payable;
154
-
155
- /// @notice Uninstall an app
156
- /// @param app The app address to uninstall
157
- /// @param account The account to uninstall the app from
158
- /// @param data The data to pass to the app's onUninstall function
159
- function uninstallApp(ITownsApp app, IAppAccount account, bytes calldata data) external;
160
-
161
- /// @notice Update an app to the latest version
162
- /// @param app The app address to update
163
- /// @param account The account to update the app to
164
- function updateApp(ITownsApp app, IAppAccount account) external;
165
-
166
143
  /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
167
144
  /* Admin */
168
145
  /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
@@ -2,10 +2,45 @@
2
2
  pragma solidity ^0.8.23;
3
3
 
4
4
  interface ITippingBase {
5
+ // =============================================================
6
+ // Enums
7
+ // =============================================================
8
+
9
+ enum TipRecipientType {
10
+ Member, // Tips to token holders
11
+ Bot, // Tips to bot wallets
12
+ Pool // Tips to pool wallets
13
+ }
14
+
5
15
  // =============================================================
6
16
  // Structs
7
17
  // =============================================================
8
18
 
19
+ struct TipMetadata {
20
+ bytes32 messageId;
21
+ bytes32 channelId;
22
+ bytes data; // Extensible metadata
23
+ }
24
+
25
+ /// @notice Params for Member tips (includes tokenId)
26
+ struct MembershipTipParams {
27
+ address receiver;
28
+ uint256 tokenId;
29
+ address currency;
30
+ uint256 amount;
31
+ TipMetadata metadata;
32
+ }
33
+
34
+ /// @notice Params for Bot tips (similar to Wallet but distinct type)
35
+ struct BotTipParams {
36
+ address receiver;
37
+ address currency;
38
+ bytes32 appId;
39
+ uint256 amount;
40
+ TipMetadata metadata;
41
+ }
42
+
43
+ /// @notice Legacy tip request (maintain backwards compatibility)
9
44
  struct TipRequest {
10
45
  address receiver;
11
46
  uint256 tokenId;
@@ -19,6 +54,16 @@ interface ITippingBase {
19
54
  // Events
20
55
  // =============================================================
21
56
 
57
+ event TipSent(
58
+ address indexed sender,
59
+ address indexed receiver,
60
+ TipRecipientType indexed recipientType,
61
+ address currency,
62
+ uint256 amount,
63
+ uint256 tokenId // 0 if not a member tip
64
+ );
65
+
66
+ // Maintain legacy event for backwards compatibility
22
67
  event Tip(
23
68
  uint256 indexed tokenId,
24
69
  address indexed currency,
@@ -33,6 +78,8 @@ interface ITippingBase {
33
78
  // Errors
34
79
  // =============================================================
35
80
 
81
+ error InvalidRecipientType();
82
+ error InvalidTipData();
36
83
  error TokenDoesNotExist();
37
84
  error ReceiverIsNotMember();
38
85
  error CannotTipSelf();
@@ -43,7 +90,16 @@ interface ITippingBase {
43
90
  }
44
91
 
45
92
  interface ITipping is ITippingBase {
46
- /// @notice Sends a tip to a space member
93
+ /// @notice Send a tip using flexible encoding based on recipient type
94
+ /// @param recipientType The type of recipient (Member, Wallet, Bot, Pool)
95
+ /// @param data ABI-encoded tip params based on recipientType:
96
+ /// - Member: abi.encode(MembershipTipParams)
97
+ /// - Wallet: abi.encode(WalletTipParams)
98
+ /// - Bot: abi.encode(BotTipParams)
99
+ /// - Pool: Reserved for future implementation
100
+ function sendTip(TipRecipientType recipientType, bytes calldata data) external payable;
101
+
102
+ /// @notice Sends a tip to a space member (legacy)
47
103
  /// @param tipRequest The tip request containing token ID, currency, amount, message ID and
48
104
  /// channel ID
49
105
  /// @dev Requires sender and receiver to be members of the space
@@ -51,6 +107,24 @@ interface ITipping is ITippingBase {
51
107
  /// @dev Emits Tip event
52
108
  function tip(TipRequest calldata tipRequest) external payable;
53
109
 
110
+ /// @notice Get tips received by wallet address and currency
111
+ /// @param wallet The wallet address to get tips for
112
+ /// @param currency The currency address to get tips in
113
+ /// @return The total amount of tips received in the specified currency
114
+ function tipsByWalletAndCurrency(
115
+ address wallet,
116
+ address currency
117
+ ) external view returns (uint256);
118
+
119
+ /// @notice Get tip count by wallet and currency
120
+ /// @param wallet The wallet address to get tip count for
121
+ /// @param currency The currency address to get tip count in
122
+ /// @return The total number of tips received in the specified currency
123
+ function tipCountByWalletAndCurrency(
124
+ address wallet,
125
+ address currency
126
+ ) external view returns (uint256);
127
+
54
128
  /// @notice Gets the total tips received for a token ID in a specific currency
55
129
  /// @param tokenId The token ID to get tips for
56
130
  /// @param currency The currency address to get tips in
@@ -2,73 +2,269 @@
2
2
  pragma solidity ^0.8.23;
3
3
 
4
4
  // interfaces
5
+ import {ITippingBase} from "./ITipping.sol";
6
+ import {ITownsPointsBase} from "../../../airdrop/points/ITownsPoints.sol";
7
+ import {IPlatformRequirements} from "../../../factory/facets/platform/requirements/IPlatformRequirements.sol";
5
8
 
6
9
  // libraries
7
10
  import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
11
+ import {BasisPoints} from "../../../utils/libraries/BasisPoints.sol";
8
12
  import {CurrencyTransfer} from "../../../utils/libraries/CurrencyTransfer.sol";
13
+ import {CustomRevert} from "../../../utils/libraries/CustomRevert.sol";
14
+ import {MembershipStorage} from "../membership/MembershipStorage.sol";
15
+ import {TippingStorage} from "./TippingStorage.sol";
9
16
 
10
17
  // contracts
18
+ import {PointsBase} from "../points/PointsBase.sol";
11
19
 
12
- library TippingBase {
20
+ abstract contract TippingBase is ITippingBase, PointsBase {
13
21
  using EnumerableSet for EnumerableSet.AddressSet;
22
+ using CustomRevert for bytes4;
14
23
 
15
- // keccak256(abi.encode(uint256(keccak256("spaces.facets.tipping.storage")) - 1)) &
16
- // ~bytes32(uint256(0xff))
17
- bytes32 internal constant STORAGE_SLOT =
18
- 0xb6cb334a9eea0cca2581db4520b45ac6f03de8e3927292302206bb82168be300;
24
+ /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
25
+ /* Internal Functions */
26
+ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
19
27
 
20
- struct TippingStats {
21
- uint256 totalTips;
22
- uint256 tipAmount;
28
+ /// @dev Processes sendTip based on recipient type
29
+ function _sendTip(TipRecipientType recipientType, bytes calldata data) internal {
30
+ if (recipientType == TipRecipientType.Member) {
31
+ _sendMemberTip(data);
32
+ } else if (recipientType == TipRecipientType.Bot) {
33
+ _sendBotTip(data);
34
+ } else {
35
+ InvalidRecipientType.selector.revertWith();
36
+ }
37
+ }
38
+
39
+ /// @dev Processes legacy tip function
40
+ function _tip(TipRequest calldata tipRequest) internal {
41
+ _validateTipRequest(
42
+ msg.sender,
43
+ tipRequest.receiver,
44
+ tipRequest.currency,
45
+ tipRequest.amount
46
+ );
47
+
48
+ uint256 tipAmount = tipRequest.amount;
49
+
50
+ // Handle native token
51
+ if (tipRequest.currency == CurrencyTransfer.NATIVE_TOKEN) {
52
+ if (msg.value != tipAmount) MsgValueMismatch.selector.revertWith();
53
+ uint256 protocolFee = _handleProtocolFee(tipAmount);
54
+ tipAmount -= protocolFee;
55
+ } else {
56
+ if (msg.value != 0) UnexpectedETH.selector.revertWith();
57
+ }
58
+
59
+ // Process tip
60
+ _processTip(
61
+ msg.sender,
62
+ tipRequest.receiver,
63
+ tipRequest.tokenId,
64
+ TipRecipientType.Member,
65
+ tipRequest.currency,
66
+ tipAmount
67
+ );
68
+
69
+ // Emit legacy event (with original amount for backwards compatibility)
70
+ emit Tip(
71
+ tipRequest.tokenId,
72
+ tipRequest.currency,
73
+ msg.sender,
74
+ tipRequest.receiver,
75
+ tipRequest.amount,
76
+ tipRequest.messageId,
77
+ tipRequest.channelId
78
+ );
79
+
80
+ // Emit new event (with actual tip amount after fees)
81
+ emit TipSent(
82
+ msg.sender,
83
+ tipRequest.receiver,
84
+ TipRecipientType.Member,
85
+ tipRequest.currency,
86
+ tipAmount,
87
+ tipRequest.tokenId
88
+ );
23
89
  }
24
90
 
25
- struct Layout {
26
- EnumerableSet.AddressSet currencies;
27
- mapping(uint256 tokenId => mapping(address currency => uint256 amount)) tipsByCurrencyByTokenId;
28
- mapping(address currency => TippingStats) tippingStatsByCurrency;
91
+ /// @dev Handles member tips
92
+ function _sendMemberTip(bytes calldata data) internal {
93
+ MembershipTipParams memory params = abi.decode(data, (MembershipTipParams));
94
+ _validateTipRequest(msg.sender, params.receiver, params.currency, params.amount);
95
+
96
+ uint256 tipAmount = params.amount;
97
+
98
+ // Handle native token
99
+ if (params.currency == CurrencyTransfer.NATIVE_TOKEN) {
100
+ if (msg.value != tipAmount) MsgValueMismatch.selector.revertWith();
101
+ uint256 protocolFee = _handleProtocolFee(tipAmount);
102
+ tipAmount -= protocolFee;
103
+ } else {
104
+ if (msg.value != 0) UnexpectedETH.selector.revertWith();
105
+ }
106
+
107
+ // Process tip
108
+ _processTip(
109
+ msg.sender,
110
+ params.receiver,
111
+ params.tokenId,
112
+ TipRecipientType.Member,
113
+ params.currency,
114
+ tipAmount
115
+ );
116
+
117
+ // Emit events
118
+ emit TipSent(
119
+ msg.sender,
120
+ params.receiver,
121
+ TipRecipientType.Member,
122
+ params.currency,
123
+ tipAmount,
124
+ params.tokenId
125
+ );
126
+
127
+ emit Tip(
128
+ params.tokenId,
129
+ params.currency,
130
+ msg.sender,
131
+ params.receiver,
132
+ tipAmount,
133
+ params.metadata.messageId,
134
+ params.metadata.channelId
135
+ );
29
136
  }
30
137
 
31
- function layout() internal pure returns (Layout storage l) {
32
- assembly {
33
- l.slot := STORAGE_SLOT
138
+ /// @dev Handles bot tips
139
+ function _sendBotTip(bytes calldata data) internal {
140
+ BotTipParams memory params = abi.decode(data, (BotTipParams));
141
+ _validateTipRequest(msg.sender, params.receiver, params.currency, params.amount);
142
+
143
+ uint256 tipAmount = params.amount;
144
+
145
+ // Handle native token (no protocol fee for bot tips)
146
+ if (params.currency == CurrencyTransfer.NATIVE_TOKEN) {
147
+ if (msg.value != tipAmount) MsgValueMismatch.selector.revertWith();
148
+ } else {
149
+ if (msg.value != 0) UnexpectedETH.selector.revertWith();
34
150
  }
151
+
152
+ // Process tip (tokenId = 0 for bot tips)
153
+ _processTip(
154
+ msg.sender,
155
+ params.receiver,
156
+ 0, // No tokenId for bot tips
157
+ TipRecipientType.Bot,
158
+ params.currency,
159
+ tipAmount
160
+ );
161
+
162
+ emit TipSent(
163
+ msg.sender,
164
+ params.receiver,
165
+ TipRecipientType.Bot,
166
+ params.currency,
167
+ tipAmount,
168
+ 0
169
+ );
35
170
  }
36
171
 
37
- function tip(
172
+ /// @dev Core tip processing logic
173
+ function _processTip(
38
174
  address sender,
39
175
  address receiver,
40
176
  uint256 tokenId,
177
+ TipRecipientType recipientType,
41
178
  address currency,
42
179
  uint256 amount
43
180
  ) internal {
44
- Layout storage ds = layout();
181
+ TippingStorage.Layout storage $ = TippingStorage.layout();
45
182
 
46
- ds.currencies.add(currency);
47
- ds.tipsByCurrencyByTokenId[tokenId][currency] += amount;
183
+ // Add currency to set
184
+ $.currencies.add(currency);
48
185
 
49
- TippingStats storage stats = ds.tippingStatsByCurrency[currency];
186
+ // Update stats by currency
187
+ TippingStorage.TippingStats storage stats = $.tippingStatsByCurrency[currency];
50
188
  stats.tipAmount += amount;
51
189
  stats.totalTips += 1;
52
190
 
191
+ // Update wallet-based tracking (all tips)
192
+ $.tippingStatsByCurrencyByWallet[receiver][currency].tipAmount += amount;
193
+ $.tippingStatsByCurrencyByWallet[receiver][currency].totalTips += 1;
194
+
195
+ // Update tokenId-based tracking (backwards compatibility, only for Member tips)
196
+ if (recipientType == TipRecipientType.Member) {
197
+ $.tipsByCurrencyByTokenId[tokenId][currency] += amount;
198
+ }
199
+
200
+ // Transfer currency
53
201
  CurrencyTransfer.transferCurrency(currency, sender, receiver, amount);
54
202
  }
55
203
 
56
- function totalTipsByCurrency(address currency) internal view returns (uint256) {
57
- return layout().tippingStatsByCurrency[currency].totalTips;
204
+ /// @dev Handles protocol fee and points minting
205
+ function _handleProtocolFee(uint256 amount) internal returns (uint256 protocolFee) {
206
+ MembershipStorage.Layout storage ds = MembershipStorage.layout();
207
+ IPlatformRequirements platform = IPlatformRequirements(ds.spaceFactory);
208
+
209
+ protocolFee = BasisPoints.calculate(amount, 50); // 0.5%
210
+
211
+ CurrencyTransfer.transferCurrency(
212
+ CurrencyTransfer.NATIVE_TOKEN,
213
+ msg.sender,
214
+ platform.getFeeRecipient(),
215
+ protocolFee
216
+ );
217
+
218
+ // Mint points
219
+ address airdropDiamond = _getAirdropDiamond();
220
+ uint256 points = _getPoints(
221
+ airdropDiamond,
222
+ ITownsPointsBase.Action.Tip,
223
+ abi.encode(protocolFee)
224
+ );
225
+ _mintPoints(airdropDiamond, msg.sender, points);
58
226
  }
59
227
 
60
- function tipAmountByCurrency(address currency) internal view returns (uint256) {
61
- return layout().tippingStatsByCurrency[currency].tipAmount;
228
+ /// @dev Validates common tip requirements
229
+ function _validateTipRequest(
230
+ address sender,
231
+ address receiver,
232
+ address currency,
233
+ uint256 amount
234
+ ) internal pure {
235
+ if (currency == address(0)) CurrencyIsZero.selector.revertWith();
236
+ if (sender == receiver) CannotTipSelf.selector.revertWith();
237
+ if (amount == 0) AmountIsZero.selector.revertWith();
62
238
  }
63
239
 
64
- function tipsByCurrencyByTokenId(
65
- uint256 tokenId,
240
+ /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
241
+ /* Internal View Functions */
242
+ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
243
+
244
+ function _getTipsByWallet(address wallet, address currency) internal view returns (uint256) {
245
+ return TippingStorage.layout().tippingStatsByCurrencyByWallet[wallet][currency].tipAmount;
246
+ }
247
+
248
+ function _getTipCountByWallet(
249
+ address wallet,
66
250
  address currency
67
251
  ) internal view returns (uint256) {
68
- return layout().tipsByCurrencyByTokenId[tokenId][currency];
252
+ return TippingStorage.layout().tippingStatsByCurrencyByWallet[wallet][currency].totalTips;
253
+ }
254
+
255
+ function _getTipsByTokenId(uint256 tokenId, address currency) internal view returns (uint256) {
256
+ return TippingStorage.layout().tipsByCurrencyByTokenId[tokenId][currency];
257
+ }
258
+
259
+ function _getTippingCurrencies() internal view returns (address[] memory) {
260
+ return TippingStorage.layout().currencies.values();
261
+ }
262
+
263
+ function _getTotalTipsByCurrency(address currency) internal view returns (uint256) {
264
+ return TippingStorage.layout().tippingStatsByCurrency[currency].totalTips;
69
265
  }
70
266
 
71
- function tippingCurrencies() internal view returns (address[] memory) {
72
- return layout().currencies.values();
267
+ function _getTipAmountByCurrency(address currency) internal view returns (uint256) {
268
+ return TippingStorage.layout().tippingStatsByCurrency[currency].tipAmount;
73
269
  }
74
270
  }
@@ -2,80 +2,48 @@
2
2
  pragma solidity ^0.8.23;
3
3
 
4
4
  // interfaces
5
- import {ITownsPointsBase} from "../../../airdrop/points/ITownsPoints.sol";
6
- import {IPlatformRequirements} from "../../../factory/facets/platform/requirements/IPlatformRequirements.sol";
7
5
  import {ITipping} from "./ITipping.sol";
8
6
 
9
7
  // libraries
10
- import {BasisPoints} from "../../../utils/libraries/BasisPoints.sol";
11
- import {CurrencyTransfer} from "../../../utils/libraries/CurrencyTransfer.sol";
12
- import {CustomRevert} from "../../../utils/libraries/CustomRevert.sol";
13
- import {MembershipStorage} from "../membership/MembershipStorage.sol";
14
- import {TippingBase} from "./TippingBase.sol";
15
8
 
16
9
  // contracts
17
10
  import {Facet} from "@towns-protocol/diamond/src/facets/Facet.sol";
18
11
  import {ReentrancyGuard} from "solady/utils/ReentrancyGuard.sol";
19
12
  import {ERC721ABase} from "../../../diamond/facets/token/ERC721A/ERC721ABase.sol";
20
- import {PointsBase} from "../points/PointsBase.sol";
21
-
22
- contract TippingFacet is ITipping, ERC721ABase, PointsBase, Facet, ReentrancyGuard {
23
- using CustomRevert for bytes4;
13
+ import {TippingBase} from "./TippingBase.sol";
24
14
 
15
+ contract TippingFacet is ITipping, TippingBase, ERC721ABase, Facet, ReentrancyGuard {
25
16
  function __Tipping_init() external onlyInitializing {
26
17
  _addInterface(type(ITipping).interfaceId);
27
18
  }
28
19
 
29
20
  /// @inheritdoc ITipping
30
- function tip(TipRequest calldata tipRequest) external payable nonReentrant {
31
- _validateTipRequest(
32
- msg.sender,
33
- tipRequest.receiver,
34
- tipRequest.currency,
35
- tipRequest.amount
36
- );
37
-
38
- uint256 tipAmount = tipRequest.amount;
39
-
40
- if (tipRequest.currency != CurrencyTransfer.NATIVE_TOKEN) {
41
- if (msg.value != 0) UnexpectedETH.selector.revertWith();
42
- } else {
43
- if (msg.value != tipAmount) MsgValueMismatch.selector.revertWith();
44
-
45
- uint256 protocolFee = _payProtocol(msg.sender, tipAmount);
46
- tipAmount -= protocolFee;
47
-
48
- address airdropDiamond = _getAirdropDiamond();
49
- uint256 points = _getPoints(
50
- airdropDiamond,
51
- ITownsPointsBase.Action.Tip,
52
- abi.encode(protocolFee)
53
- );
54
- _mintPoints(airdropDiamond, msg.sender, points);
55
- }
21
+ function sendTip(
22
+ TipRecipientType recipientType,
23
+ bytes calldata data
24
+ ) external payable nonReentrant {
25
+ _sendTip(recipientType, data);
26
+ }
56
27
 
57
- TippingBase.tip(
58
- msg.sender,
59
- tipRequest.receiver,
60
- tipRequest.tokenId,
61
- tipRequest.currency,
62
- tipAmount
63
- );
28
+ /// @inheritdoc ITipping
29
+ function tip(TipRequest calldata tipRequest) external payable nonReentrant {
30
+ _tip(tipRequest);
31
+ }
64
32
 
65
- emit Tip(
66
- tipRequest.tokenId,
67
- tipRequest.currency,
68
- msg.sender,
69
- tipRequest.receiver,
70
- tipRequest.amount,
71
- tipRequest.messageId,
72
- tipRequest.channelId
73
- );
33
+ /// @inheritdoc ITipping
34
+ function tipsByWalletAndCurrency(
35
+ address wallet,
36
+ address currency
37
+ ) external view returns (uint256) {
38
+ return _getTipsByWallet(wallet, currency);
74
39
  }
75
40
 
76
41
  /// @inheritdoc ITipping
77
- function tippingCurrencies() external view returns (address[] memory) {
78
- return TippingBase.tippingCurrencies();
42
+ function tipCountByWalletAndCurrency(
43
+ address wallet,
44
+ address currency
45
+ ) external view returns (uint256) {
46
+ return _getTipCountByWallet(wallet, currency);
79
47
  }
80
48
 
81
49
  /// @inheritdoc ITipping
@@ -83,45 +51,21 @@ contract TippingFacet is ITipping, ERC721ABase, PointsBase, Facet, ReentrancyGua
83
51
  uint256 tokenId,
84
52
  address currency
85
53
  ) external view returns (uint256) {
86
- return TippingBase.tipsByCurrencyByTokenId(tokenId, currency);
54
+ return _getTipsByTokenId(tokenId, currency);
87
55
  }
88
56
 
89
57
  /// @inheritdoc ITipping
90
- function totalTipsByCurrency(address currency) external view returns (uint256) {
91
- return TippingBase.totalTipsByCurrency(currency);
58
+ function tippingCurrencies() external view returns (address[] memory) {
59
+ return _getTippingCurrencies();
92
60
  }
93
61
 
94
62
  /// @inheritdoc ITipping
95
- function tipAmountByCurrency(address currency) external view returns (uint256) {
96
- return TippingBase.tipAmountByCurrency(currency);
97
- }
98
-
99
- /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
100
- /* Internal */
101
- /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
102
-
103
- function _validateTipRequest(
104
- address sender,
105
- address receiver,
106
- address currency,
107
- uint256 amount
108
- ) internal pure {
109
- if (currency == address(0)) CurrencyIsZero.selector.revertWith();
110
- if (sender == receiver) CannotTipSelf.selector.revertWith();
111
- if (amount == 0) AmountIsZero.selector.revertWith();
63
+ function totalTipsByCurrency(address currency) external view returns (uint256) {
64
+ return _getTotalTipsByCurrency(currency);
112
65
  }
113
66
 
114
- function _payProtocol(address sender, uint256 amount) internal returns (uint256 protocolFee) {
115
- MembershipStorage.Layout storage ds = MembershipStorage.layout();
116
- IPlatformRequirements platform = IPlatformRequirements(ds.spaceFactory);
117
-
118
- protocolFee = BasisPoints.calculate(amount, 50); // 0.5%
119
-
120
- CurrencyTransfer.transferCurrency(
121
- CurrencyTransfer.NATIVE_TOKEN,
122
- sender,
123
- platform.getFeeRecipient(),
124
- protocolFee
125
- );
67
+ /// @inheritdoc ITipping
68
+ function tipAmountByCurrency(address currency) external view returns (uint256) {
69
+ return _getTipAmountByCurrency(currency);
126
70
  }
127
71
  }
@@ -0,0 +1,30 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.23;
3
+
4
+ // libraries
5
+ import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
6
+
7
+ library TippingStorage {
8
+ // keccak256(abi.encode(uint256(keccak256("spaces.facets.tipping.storage")) - 1)) &
9
+ // ~bytes32(uint256(0xff))
10
+ bytes32 internal constant STORAGE_SLOT =
11
+ 0xb6cb334a9eea0cca2581db4520b45ac6f03de8e3927292302206bb82168be300;
12
+
13
+ struct TippingStats {
14
+ uint256 totalTips;
15
+ uint256 tipAmount;
16
+ }
17
+
18
+ struct Layout {
19
+ EnumerableSet.AddressSet currencies;
20
+ mapping(uint256 tokenId => mapping(address currency => uint256 amount)) tipsByCurrencyByTokenId;
21
+ mapping(address currency => TippingStats) tippingStatsByCurrency;
22
+ mapping(address wallet => mapping(address currency => TippingStats)) tippingStatsByCurrencyByWallet;
23
+ }
24
+
25
+ function layout() internal pure returns (Layout storage l) {
26
+ assembly {
27
+ l.slot := STORAGE_SLOT
28
+ }
29
+ }
30
+ }