@towns-protocol/contracts 0.0.374 → 0.0.375
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 +3 -3
- package/scripts/deployments/facets/DeployTipping.s.sol +17 -6
- package/src/spaces/facets/tipping/ITipping.sol +75 -1
- package/src/spaces/facets/tipping/TippingBase.sol +225 -29
- package/src/spaces/facets/tipping/TippingFacet.sol +31 -87
- package/src/spaces/facets/tipping/TippingStorage.sol +30 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@towns-protocol/contracts",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.375",
|
|
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.
|
|
38
|
+
"@towns-protocol/prettier-config": "^0.0.375",
|
|
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": "
|
|
60
|
+
"gitHead": "f64537fbf8d7eca815c5c5e52f601a387e713a0a"
|
|
61
61
|
}
|
|
@@ -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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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(
|
|
@@ -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
|
|
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
|
-
|
|
20
|
+
abstract contract TippingBase is ITippingBase, PointsBase {
|
|
13
21
|
using EnumerableSet for EnumerableSet.AddressSet;
|
|
22
|
+
using CustomRevert for bytes4;
|
|
14
23
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
0xb6cb334a9eea0cca2581db4520b45ac6f03de8e3927292302206bb82168be300;
|
|
24
|
+
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
|
|
25
|
+
/* Internal Functions */
|
|
26
|
+
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
|
|
19
27
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
|
181
|
+
TippingStorage.Layout storage $ = TippingStorage.layout();
|
|
45
182
|
|
|
46
|
-
|
|
47
|
-
|
|
183
|
+
// Add currency to set
|
|
184
|
+
$.currencies.add(currency);
|
|
48
185
|
|
|
49
|
-
|
|
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
|
-
|
|
57
|
-
|
|
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
|
-
|
|
61
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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().
|
|
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
|
|
72
|
-
return layout().
|
|
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 {
|
|
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
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
78
|
-
|
|
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
|
|
54
|
+
return _getTipsByTokenId(tokenId, currency);
|
|
87
55
|
}
|
|
88
56
|
|
|
89
57
|
/// @inheritdoc ITipping
|
|
90
|
-
function
|
|
91
|
-
return
|
|
58
|
+
function tippingCurrencies() external view returns (address[] memory) {
|
|
59
|
+
return _getTippingCurrencies();
|
|
92
60
|
}
|
|
93
61
|
|
|
94
62
|
/// @inheritdoc ITipping
|
|
95
|
-
function
|
|
96
|
-
return
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
+
}
|