@totems/evm 1.0.5 → 1.0.8
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/contracts/ProxyMod.sol +331 -0
- package/package.json +3 -10
- package/test/helpers.d.ts +10 -2
- package/test/helpers.ts +467 -0
- package/test/constants.d.ts +0 -11
- package/test/constants.js +0 -17
- package/test/helpers.js +0 -300
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
pragma solidity ^0.8.28;
|
|
3
|
+
|
|
4
|
+
import "../mods/TotemMod.sol";
|
|
5
|
+
import "../interfaces/ITotemTypes.sol";
|
|
6
|
+
import {TotemsLibrary} from "../mods/TotemsLibrary.sol";
|
|
7
|
+
import {IMarket} from "../interfaces/IMarket.sol";
|
|
8
|
+
import {ITotems} from "../interfaces/ITotems.sol";
|
|
9
|
+
import {ReentrancyGuard} from "./ReentrancyGuard.sol";
|
|
10
|
+
import {Shared} from "./Shared.sol";
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
contract ProxyMod is IModTransfer, IModMint, IModMinter, IModBurn, ReentrancyGuard {
|
|
14
|
+
|
|
15
|
+
// Can't use ModBase here because it's deployed BEFORE the totems contract,
|
|
16
|
+
// so need to re-roll some of that here.
|
|
17
|
+
receive() external payable {}
|
|
18
|
+
fallback() external payable {}
|
|
19
|
+
address payable private seller;
|
|
20
|
+
address public marketContract;
|
|
21
|
+
address public totemsContract;
|
|
22
|
+
|
|
23
|
+
mapping(bytes32 => mapping(ITotemTypes.Hook => mapping(address => bool))) internal isModEnabled;
|
|
24
|
+
mapping(bytes32 => ITotemTypes.TotemMods) internal totemMods;
|
|
25
|
+
bytes32[] public totemsWithMods;
|
|
26
|
+
|
|
27
|
+
function getSeller() external view returns (address payable) {
|
|
28
|
+
return seller != address(0) ? payable(address(seller)) : payable(address(this));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
modifier onlyTotems() {
|
|
32
|
+
require(msg.sender == totemsContract, "Only Totems contract");
|
|
33
|
+
_;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
constructor(address payable _seller) {
|
|
37
|
+
require(_seller != address(0), "Invalid seller address");
|
|
38
|
+
seller = _seller;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function initialize(
|
|
42
|
+
address _totemsContract,
|
|
43
|
+
address _marketContract
|
|
44
|
+
) external {
|
|
45
|
+
require(msg.sender == seller, "Only seller can initialize");
|
|
46
|
+
require(totemsContract == address(0), "Already initialized");
|
|
47
|
+
require(_totemsContract != address(0), "Invalid totems contract");
|
|
48
|
+
require(_marketContract != address(0), "Invalid market contract");
|
|
49
|
+
|
|
50
|
+
totemsContract = _totemsContract;
|
|
51
|
+
marketContract = _marketContract;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ========== Errors ==========
|
|
55
|
+
error CantUseCreatedHook();
|
|
56
|
+
error UnknownHook();
|
|
57
|
+
error ModNotEnabledForMint();
|
|
58
|
+
error InvalidAddressLength();
|
|
59
|
+
error InvalidHexCharacter();
|
|
60
|
+
error NoFeeRequired();
|
|
61
|
+
|
|
62
|
+
// ========== Managerial Functions ==========
|
|
63
|
+
|
|
64
|
+
function addMod(
|
|
65
|
+
string calldata ticker,
|
|
66
|
+
ITotemTypes.Hook[] calldata hooks,
|
|
67
|
+
address mod,
|
|
68
|
+
address payable referrer
|
|
69
|
+
) external payable nonReentrant {
|
|
70
|
+
ITotems totems = ITotems(totemsContract);
|
|
71
|
+
IMarket market = IMarket(marketContract);
|
|
72
|
+
|
|
73
|
+
require(
|
|
74
|
+
totems.getTotem(ticker).creator == msg.sender,
|
|
75
|
+
"Only the totem creator can add mods"
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
bytes32 tickerBytes = TotemsLibrary.tickerToBytes(ticker);
|
|
79
|
+
|
|
80
|
+
if(!totems.isLicensed(ticker, mod)) {
|
|
81
|
+
uint256 modFee = market.getModFee(mod);
|
|
82
|
+
uint256 referrerFee = totems.getFee(referrer);
|
|
83
|
+
uint256 totalFee = modFee + referrerFee;
|
|
84
|
+
require(msg.value >= totalFee, "Insufficient fee");
|
|
85
|
+
totems.setLicenseFromProxy(tickerBytes, mod);
|
|
86
|
+
|
|
87
|
+
// Pay mod seller
|
|
88
|
+
ITotemTypes.Mod memory totemMod = market.getMod(mod);
|
|
89
|
+
if(modFee > 0) {
|
|
90
|
+
Shared.safeTransfer(totemMod.seller, modFee);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Pay referrer (or burn if no referrer)
|
|
94
|
+
if(referrerFee > 0) {
|
|
95
|
+
address recipient = referrer != address(0) ? referrer : address(0);
|
|
96
|
+
Shared.safeTransfer(recipient, referrerFee);
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
if(msg.value > 0) revert NoFeeRequired();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
for (uint256 i = 0; i < hooks.length; i++) {
|
|
103
|
+
ITotemTypes.Hook hook = hooks[i];
|
|
104
|
+
|
|
105
|
+
if (isModEnabled[tickerBytes][hook][mod]) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
isModEnabled[tickerBytes][hook][mod] = true;
|
|
110
|
+
|
|
111
|
+
_pushModToHook(tickerBytes, hook, mod);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function removeMod(
|
|
116
|
+
string calldata ticker,
|
|
117
|
+
address mod
|
|
118
|
+
) external {
|
|
119
|
+
require(
|
|
120
|
+
TotemsLibrary.getCreator(totemsContract, ticker) == msg.sender,
|
|
121
|
+
"Only totem creator"
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
bytes32 tickerBytes = TotemsLibrary.tickerToBytes(ticker);
|
|
125
|
+
|
|
126
|
+
_removeFromHook(tickerBytes, ITotemTypes.Hook.Transfer, mod);
|
|
127
|
+
_removeFromHook(tickerBytes, ITotemTypes.Hook.Mint, mod);
|
|
128
|
+
_removeFromHook(tickerBytes, ITotemTypes.Hook.Burn, mod);
|
|
129
|
+
_removeFromHook(tickerBytes, ITotemTypes.Hook.Created, mod);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
// ========== Mod Hook Functions ==========
|
|
134
|
+
|
|
135
|
+
function onTransfer(
|
|
136
|
+
string calldata ticker,
|
|
137
|
+
address from,
|
|
138
|
+
address to,
|
|
139
|
+
uint256 amount,
|
|
140
|
+
string calldata memo
|
|
141
|
+
) external override onlyTotems {
|
|
142
|
+
TotemsLibrary.checkLicense(totemsContract, ticker);
|
|
143
|
+
|
|
144
|
+
bytes32 tickerBytes = TotemsLibrary.tickerToBytes(ticker);
|
|
145
|
+
address[] memory mods = totemMods[tickerBytes].transfer;
|
|
146
|
+
uint256 length = mods.length;
|
|
147
|
+
|
|
148
|
+
for (uint256 i = 0; i < length; i++) {
|
|
149
|
+
IModTransfer(mods[i]).onTransfer(
|
|
150
|
+
ticker,
|
|
151
|
+
from,
|
|
152
|
+
to,
|
|
153
|
+
amount,
|
|
154
|
+
memo
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function onMint(
|
|
160
|
+
string calldata ticker,
|
|
161
|
+
address minter,
|
|
162
|
+
uint256 amount,
|
|
163
|
+
uint256 payment,
|
|
164
|
+
string calldata memo
|
|
165
|
+
) external override onlyTotems {
|
|
166
|
+
TotemsLibrary.checkLicense(totemsContract, ticker);
|
|
167
|
+
|
|
168
|
+
bytes32 tickerBytes = TotemsLibrary.tickerToBytes(ticker);
|
|
169
|
+
address[] memory mods = totemMods[tickerBytes].mint;
|
|
170
|
+
uint256 length = mods.length;
|
|
171
|
+
|
|
172
|
+
for (uint256 i = 0; i < length; i++) {
|
|
173
|
+
IModMint(mods[i]).onMint(
|
|
174
|
+
ticker,
|
|
175
|
+
minter,
|
|
176
|
+
amount,
|
|
177
|
+
payment,
|
|
178
|
+
memo
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
function mint(
|
|
185
|
+
string calldata ticker,
|
|
186
|
+
address minter,
|
|
187
|
+
uint256 amount,
|
|
188
|
+
string calldata memo
|
|
189
|
+
) external onlyTotems payable {
|
|
190
|
+
TotemsLibrary.checkLicense(totemsContract, ticker);
|
|
191
|
+
|
|
192
|
+
// This likely eliminates some use cases, but it's necessary to avoid
|
|
193
|
+
// ambiguity in which mod to use for minting when multiple are present,
|
|
194
|
+
// especially when payment is involved and to align with the core mint->mod logic.
|
|
195
|
+
address mod = _stringToAddress(memo);
|
|
196
|
+
|
|
197
|
+
bytes32 tickerBytes = TotemsLibrary.tickerToBytes(ticker);
|
|
198
|
+
if(!isModEnabled[tickerBytes][ITotemTypes.Hook.Mint][mod]) {
|
|
199
|
+
revert ModNotEnabledForMint();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
IModMinter(mod).mint{value: msg.value}(
|
|
203
|
+
ticker,
|
|
204
|
+
minter,
|
|
205
|
+
amount,
|
|
206
|
+
memo
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function onBurn(
|
|
211
|
+
string calldata ticker,
|
|
212
|
+
address owner,
|
|
213
|
+
uint256 amount,
|
|
214
|
+
string calldata memo
|
|
215
|
+
) external override onlyTotems {
|
|
216
|
+
TotemsLibrary.checkLicense(totemsContract, ticker);
|
|
217
|
+
|
|
218
|
+
bytes32 tickerBytes = TotemsLibrary.tickerToBytes(ticker);
|
|
219
|
+
address[] memory mods = totemMods[tickerBytes].burn;
|
|
220
|
+
uint256 length = mods.length;
|
|
221
|
+
|
|
222
|
+
for (uint256 i = 0; i < length; i++) {
|
|
223
|
+
IModBurn(mods[i]).onBurn(
|
|
224
|
+
ticker,
|
|
225
|
+
owner,
|
|
226
|
+
amount,
|
|
227
|
+
memo
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
// ========== Internal Functions ==========
|
|
239
|
+
|
|
240
|
+
function _stringToAddress(string memory str) internal pure returns (address) {
|
|
241
|
+
bytes memory strBytes = bytes(str);
|
|
242
|
+
if(strBytes.length != 42){
|
|
243
|
+
revert InvalidAddressLength();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
bytes memory addrBytes = new bytes(20);
|
|
247
|
+
|
|
248
|
+
for (uint i = 0; i < 20; i++) {
|
|
249
|
+
addrBytes[i] = bytes1(
|
|
250
|
+
_hexCharToByte(strBytes[2 + i * 2]) * 16 +
|
|
251
|
+
_hexCharToByte(strBytes[3 + i * 2])
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
address addr;
|
|
256
|
+
assembly {
|
|
257
|
+
addr := mload(add(addrBytes, 20))
|
|
258
|
+
}
|
|
259
|
+
return addr;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function _hexCharToByte(bytes1 char) internal pure returns (uint8) {
|
|
263
|
+
uint8 byteValue = uint8(char);
|
|
264
|
+
if (byteValue >= uint8(bytes1('0')) && byteValue <= uint8(bytes1('9'))) {
|
|
265
|
+
return byteValue - uint8(bytes1('0'));
|
|
266
|
+
} else if (byteValue >= uint8(bytes1('a')) && byteValue <= uint8(bytes1('f'))) {
|
|
267
|
+
return 10 + byteValue - uint8(bytes1('a'));
|
|
268
|
+
} else if (byteValue >= uint8(bytes1('A')) && byteValue <= uint8(bytes1('F'))) {
|
|
269
|
+
return 10 + byteValue - uint8(bytes1('A'));
|
|
270
|
+
}
|
|
271
|
+
revert InvalidHexCharacter();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function _pushModToHook(
|
|
275
|
+
bytes32 ticker,
|
|
276
|
+
ITotemTypes.Hook hook,
|
|
277
|
+
address mod
|
|
278
|
+
) internal {
|
|
279
|
+
ITotemTypes.TotemMods storage mods = totemMods[ticker];
|
|
280
|
+
|
|
281
|
+
if (hook == ITotemTypes.Hook.Transfer) {
|
|
282
|
+
mods.transfer.push(mod);
|
|
283
|
+
} else if (hook == ITotemTypes.Hook.Mint) {
|
|
284
|
+
mods.mint.push(mod);
|
|
285
|
+
} else if (hook == ITotemTypes.Hook.Burn) {
|
|
286
|
+
mods.burn.push(mod);
|
|
287
|
+
} else if (hook == ITotemTypes.Hook.Created) {
|
|
288
|
+
revert CantUseCreatedHook();
|
|
289
|
+
} else {
|
|
290
|
+
revert UnknownHook();
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function _removeFromHook(
|
|
295
|
+
bytes32 ticker,
|
|
296
|
+
ITotemTypes.Hook hook,
|
|
297
|
+
address mod
|
|
298
|
+
) internal {
|
|
299
|
+
if (!isModEnabled[ticker][hook][mod]) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
delete isModEnabled[ticker][hook][mod];
|
|
304
|
+
_removeModFromHookArray(ticker, hook, mod);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function _removeModFromHookArray(
|
|
308
|
+
bytes32 ticker,
|
|
309
|
+
ITotemTypes.Hook hook,
|
|
310
|
+
address mod
|
|
311
|
+
) internal {
|
|
312
|
+
address[] storage arr;
|
|
313
|
+
|
|
314
|
+
if (hook == ITotemTypes.Hook.Transfer) arr = totemMods[ticker].transfer;
|
|
315
|
+
else if (hook == ITotemTypes.Hook.Mint) arr = totemMods[ticker].mint;
|
|
316
|
+
else if (hook == ITotemTypes.Hook.Burn) arr = totemMods[ticker].burn;
|
|
317
|
+
else if (hook == ITotemTypes.Hook.Created) arr = totemMods[ticker].created;
|
|
318
|
+
else revert("Invalid hook");
|
|
319
|
+
|
|
320
|
+
uint256 length = arr.length;
|
|
321
|
+
for (uint256 i = 0; i < length; i++) {
|
|
322
|
+
if (arr[i] == mod) {
|
|
323
|
+
arr[i] = arr[length - 1];
|
|
324
|
+
arr.pop();
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@totems/evm",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
4
|
+
"type": "module",
|
|
4
5
|
"description": "Totems EVM smart contracts for building modular token systems",
|
|
5
6
|
"author": "nsjames",
|
|
6
7
|
"license": "MIT & AGPL-3.0",
|
|
@@ -26,17 +27,9 @@
|
|
|
26
27
|
"test/**/*"
|
|
27
28
|
],
|
|
28
29
|
"exports": {
|
|
29
|
-
"./test/constants": {
|
|
30
|
-
"types": "./test/constants.d.ts",
|
|
31
|
-
"import": "./test/constants.js",
|
|
32
|
-
"require": "./test/constants.js",
|
|
33
|
-
"default": "./test/constants.js"
|
|
34
|
-
},
|
|
35
30
|
"./test/helpers": {
|
|
36
31
|
"types": "./test/helpers.d.ts",
|
|
37
|
-
"
|
|
38
|
-
"require": "./test/helpers.js",
|
|
39
|
-
"default": "./test/helpers.js"
|
|
32
|
+
"default": "./test/helpers.ts"
|
|
40
33
|
},
|
|
41
34
|
"./contracts/*": "./contracts/*",
|
|
42
35
|
"./interfaces/*": "./interfaces/*",
|
package/test/helpers.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
export
|
|
2
|
-
export type { HookType } from "./constants.js";
|
|
1
|
+
export declare const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
|
|
3
2
|
/**
|
|
4
3
|
* Computes the 4-byte selector for a custom error signature
|
|
5
4
|
* @param signature Error signature like "NotLicensed()" or "InsufficientBalance(uint256,uint256)"
|
|
@@ -26,6 +25,15 @@ export declare const ErrorSelectors: {
|
|
|
26
25
|
InsufficientBalance: string;
|
|
27
26
|
CantSetLicense: string;
|
|
28
27
|
};
|
|
28
|
+
export declare const MIN_BASE_FEE = 500000000000000n;
|
|
29
|
+
export declare const BURNED_FEE = 100000000000000n;
|
|
30
|
+
export declare const Hook: {
|
|
31
|
+
readonly Created: 0;
|
|
32
|
+
readonly Mint: 1;
|
|
33
|
+
readonly Burn: 2;
|
|
34
|
+
readonly Transfer: 3;
|
|
35
|
+
readonly TransferOwnership: 4;
|
|
36
|
+
};
|
|
29
37
|
export declare const setupTotemsTest: (minBaseFee?: bigint, burnedFee?: bigint) => Promise<{
|
|
30
38
|
viem: any;
|
|
31
39
|
publicClient: any;
|
package/test/helpers.ts
ADDED
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
import {network} from "hardhat";
|
|
2
|
+
import { keccak256, toBytes, decodeErrorResult, Abi } from "viem";
|
|
3
|
+
|
|
4
|
+
export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Computes the 4-byte selector for a custom error signature
|
|
8
|
+
* @param signature Error signature like "NotLicensed()" or "InsufficientBalance(uint256,uint256)"
|
|
9
|
+
*/
|
|
10
|
+
export function errorSelector(signature: string): string {
|
|
11
|
+
return keccak256(toBytes(signature)).slice(0, 10); // 0x + 8 hex chars = 4 bytes
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Extracts the error selector from a caught error's revert data
|
|
16
|
+
*/
|
|
17
|
+
function getErrorData(error: any): string | null {
|
|
18
|
+
// Try common paths where revert data might be
|
|
19
|
+
const data = error?.cause?.cause?.data
|
|
20
|
+
|| error?.cause?.data
|
|
21
|
+
|| error?.data
|
|
22
|
+
|| error?.message?.match(/return data: (0x[a-fA-F0-9]+)/)?.[1]
|
|
23
|
+
|| error?.message?.match(/data: (0x[a-fA-F0-9]+)/)?.[1];
|
|
24
|
+
return data || null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Asserts that a promise rejects with a specific custom error
|
|
29
|
+
* @param promise The promise to test
|
|
30
|
+
* @param expectedSelector The expected error selector (use errorSelector() to compute)
|
|
31
|
+
* @param errorName Human-readable error name for assertion messages
|
|
32
|
+
*/
|
|
33
|
+
export async function expectCustomError(
|
|
34
|
+
promise: Promise<any>,
|
|
35
|
+
expectedSelector: string,
|
|
36
|
+
errorName: string
|
|
37
|
+
): Promise<void> {
|
|
38
|
+
try {
|
|
39
|
+
await promise;
|
|
40
|
+
throw new Error(`Expected ${errorName} but transaction succeeded`);
|
|
41
|
+
} catch (e: any) {
|
|
42
|
+
if (e.message?.startsWith(`Expected ${errorName}`)) throw e;
|
|
43
|
+
|
|
44
|
+
const data = getErrorData(e);
|
|
45
|
+
if (!data) {
|
|
46
|
+
throw new Error(`Expected ${errorName} but got error without revert data: ${e.message}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const actualSelector = data.slice(0, 10).toLowerCase();
|
|
50
|
+
const expected = expectedSelector.toLowerCase();
|
|
51
|
+
|
|
52
|
+
if (actualSelector !== expected) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`Expected ${errorName} (${expected}) but got selector ${actualSelector}\nFull data: ${data}`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Asserts that a promise rejects with a string revert message
|
|
62
|
+
* Error(string) selector is 0x08c379a0
|
|
63
|
+
*/
|
|
64
|
+
export async function expectRevertMessage(
|
|
65
|
+
promise: Promise<any>,
|
|
66
|
+
expectedMessage: string | RegExp
|
|
67
|
+
): Promise<void> {
|
|
68
|
+
const ERROR_STRING_SELECTOR = "0x08c379a0";
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
await promise;
|
|
72
|
+
throw new Error(`Expected revert with "${expectedMessage}" but transaction succeeded`);
|
|
73
|
+
} catch (e: any) {
|
|
74
|
+
if (e.message?.startsWith("Expected revert")) throw e;
|
|
75
|
+
|
|
76
|
+
const data = getErrorData(e);
|
|
77
|
+
if (!data) {
|
|
78
|
+
// Fallback to checking error message directly
|
|
79
|
+
const matches = typeof expectedMessage === 'string'
|
|
80
|
+
? e.message?.includes(expectedMessage)
|
|
81
|
+
: expectedMessage.test(e.message);
|
|
82
|
+
if (!matches) {
|
|
83
|
+
throw new Error(`Expected revert with "${expectedMessage}" but got: ${e.message}`);
|
|
84
|
+
}
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const selector = data.slice(0, 10).toLowerCase();
|
|
89
|
+
if (selector !== ERROR_STRING_SELECTOR) {
|
|
90
|
+
// Not a string error, check if message is in the raw error
|
|
91
|
+
const matches = typeof expectedMessage === 'string'
|
|
92
|
+
? e.message?.includes(expectedMessage)
|
|
93
|
+
: expectedMessage.test(e.message);
|
|
94
|
+
if (!matches) {
|
|
95
|
+
throw new Error(`Expected string revert but got custom error with selector ${selector}`);
|
|
96
|
+
}
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Decode the string from the ABI-encoded data
|
|
101
|
+
// Format: selector (4 bytes) + offset (32 bytes) + length (32 bytes) + string data
|
|
102
|
+
try {
|
|
103
|
+
const abi: Abi = [{
|
|
104
|
+
type: 'error',
|
|
105
|
+
name: 'Error',
|
|
106
|
+
inputs: [{ name: 'message', type: 'string' }]
|
|
107
|
+
}];
|
|
108
|
+
const decoded = decodeErrorResult({ abi, data: data as `0x${string}` });
|
|
109
|
+
const message = (decoded.args as string[])[0];
|
|
110
|
+
|
|
111
|
+
const matches = typeof expectedMessage === 'string'
|
|
112
|
+
? message.includes(expectedMessage)
|
|
113
|
+
: expectedMessage.test(message);
|
|
114
|
+
|
|
115
|
+
if (!matches) {
|
|
116
|
+
throw new Error(`Expected revert with "${expectedMessage}" but got "${message}"`);
|
|
117
|
+
}
|
|
118
|
+
} catch (decodeError) {
|
|
119
|
+
// If decoding fails, fall back to checking error message
|
|
120
|
+
const matches = typeof expectedMessage === 'string'
|
|
121
|
+
? e.message?.includes(expectedMessage)
|
|
122
|
+
: expectedMessage.test(e.message);
|
|
123
|
+
if (!matches) {
|
|
124
|
+
throw new Error(`Expected revert with "${expectedMessage}" but decoding failed: ${e.message}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Pre-computed selectors for common errors
|
|
131
|
+
export const ErrorSelectors = {
|
|
132
|
+
// TotemMod errors
|
|
133
|
+
InvalidModEventOrigin: errorSelector("InvalidModEventOrigin()"),
|
|
134
|
+
NotLicensed: errorSelector("NotLicensed()"),
|
|
135
|
+
|
|
136
|
+
// Totems errors
|
|
137
|
+
Unauthorized: errorSelector("Unauthorized()"),
|
|
138
|
+
TotemNotFound: errorSelector("TotemNotFound(string)"),
|
|
139
|
+
TotemNotActive: errorSelector("TotemNotActive()"),
|
|
140
|
+
InsufficientBalance: errorSelector("InsufficientBalance(uint256,uint256)"),
|
|
141
|
+
CantSetLicense: errorSelector("CantSetLicense()"),
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
export const MIN_BASE_FEE = 500000000000000n; // 0.0005 ether
|
|
145
|
+
export const BURNED_FEE = 100000000000000n; // 0.0001 ether
|
|
146
|
+
|
|
147
|
+
export const Hook = {
|
|
148
|
+
Created: 0,
|
|
149
|
+
Mint: 1,
|
|
150
|
+
Burn: 2,
|
|
151
|
+
Transfer: 3,
|
|
152
|
+
TransferOwnership: 4,
|
|
153
|
+
} as const;
|
|
154
|
+
|
|
155
|
+
export const setupTotemsTest = async (minBaseFee: bigint = MIN_BASE_FEE, burnedFee: bigint = BURNED_FEE) => {
|
|
156
|
+
const { viem } = await network.connect() as any;
|
|
157
|
+
const publicClient = await viem.getPublicClient();
|
|
158
|
+
// @ts-ignore
|
|
159
|
+
const walletClient = await viem.getWalletClient();
|
|
160
|
+
|
|
161
|
+
const addresses = await walletClient.getAddresses();
|
|
162
|
+
const proxyModInitializer = addresses[0];
|
|
163
|
+
const proxyMod = await viem.deployContract("ProxyMod", [
|
|
164
|
+
proxyModInitializer
|
|
165
|
+
]);
|
|
166
|
+
|
|
167
|
+
let market = await viem.deployContract("ModMarket", [minBaseFee, burnedFee]);
|
|
168
|
+
let totems:any = await viem.deployContract("Totems", [
|
|
169
|
+
market.address,
|
|
170
|
+
proxyMod.address,
|
|
171
|
+
minBaseFee,
|
|
172
|
+
burnedFee,
|
|
173
|
+
]);
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
// using these to validate the interfaces
|
|
177
|
+
totems = await viem.getContractAt("ITotems", totems.address);
|
|
178
|
+
// @ts-ignore
|
|
179
|
+
market = await viem.getContractAt("IMarket", market.address);
|
|
180
|
+
// initialize proxy mod
|
|
181
|
+
await proxyMod.write.initialize([totems.address, market.address], { account: proxyModInitializer });
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
viem,
|
|
185
|
+
publicClient,
|
|
186
|
+
market,
|
|
187
|
+
totems,
|
|
188
|
+
accounts: addresses.slice(0, addresses.length),
|
|
189
|
+
proxyModSeller: addresses[0],
|
|
190
|
+
proxyMod,
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
export const modDetails = (details?:any) => Object.assign({
|
|
197
|
+
name: "Test Mod",
|
|
198
|
+
summary: "A test mod",
|
|
199
|
+
markdown: "## Test Mod\nThis is a test mod.",
|
|
200
|
+
image: "https://example.com/image.png",
|
|
201
|
+
website: "https://example.com",
|
|
202
|
+
websiteTickerPath: "/path/to/{ticker}",
|
|
203
|
+
isMinter: false,
|
|
204
|
+
needsUnlimited: false,
|
|
205
|
+
}, details || {});
|
|
206
|
+
|
|
207
|
+
export const publishMod = async (
|
|
208
|
+
market:any,
|
|
209
|
+
seller:string,
|
|
210
|
+
contract:string,
|
|
211
|
+
hooks:number[] = [],
|
|
212
|
+
details = modDetails(),
|
|
213
|
+
requiredActions:any[] = [],
|
|
214
|
+
referrer = ZERO_ADDRESS,
|
|
215
|
+
price = 1_000_000n,
|
|
216
|
+
fee = undefined
|
|
217
|
+
) => {
|
|
218
|
+
fee = fee ?? await market.read.getFee([referrer]);
|
|
219
|
+
|
|
220
|
+
return market.write.publish([
|
|
221
|
+
contract,
|
|
222
|
+
hooks,
|
|
223
|
+
price,
|
|
224
|
+
details,
|
|
225
|
+
requiredActions,
|
|
226
|
+
referrer,
|
|
227
|
+
], { value: fee, account: seller });
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export const totemDetails = (ticker:string, decimals:number) => {
|
|
231
|
+
return {
|
|
232
|
+
ticker: ticker,
|
|
233
|
+
decimals: decimals,
|
|
234
|
+
name: `${ticker} Totem`,
|
|
235
|
+
description: `This is the ${ticker} totem.`,
|
|
236
|
+
image: `https://example.com/${ticker.toLowerCase()}.png`,
|
|
237
|
+
website: `https://example.com/${ticker.toLowerCase()}`,
|
|
238
|
+
seed: '0x1110762033e7a10db4502359a19a61eb81312834769b8419047a2c9ae03ee847',
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export const createTotem = async (
|
|
243
|
+
totems:any,
|
|
244
|
+
market:any,
|
|
245
|
+
creator:string,
|
|
246
|
+
ticker:string,
|
|
247
|
+
decimals:number,
|
|
248
|
+
allocations:any[],
|
|
249
|
+
mods?:{
|
|
250
|
+
transfer?:string[],
|
|
251
|
+
mint?:string[],
|
|
252
|
+
burn?:string[],
|
|
253
|
+
created?:string[],
|
|
254
|
+
transferOwnership?:string[]
|
|
255
|
+
},
|
|
256
|
+
referrer:string = ZERO_ADDRESS,
|
|
257
|
+
details:any = undefined,
|
|
258
|
+
) => {
|
|
259
|
+
const baseFee = await totems.read.getFee([referrer]);
|
|
260
|
+
|
|
261
|
+
const _mods = Object.assign({
|
|
262
|
+
transfer: [],
|
|
263
|
+
mint: [],
|
|
264
|
+
burn: [],
|
|
265
|
+
created: [],
|
|
266
|
+
transferOwnership: [],
|
|
267
|
+
}, mods || {});
|
|
268
|
+
const uniqueMods = new Set<string>();
|
|
269
|
+
Object.values(_mods).forEach((modList:any[]) => {
|
|
270
|
+
modList.forEach(m => uniqueMods.add(m));
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
const modsFee = await market.read.getModsFee([[...uniqueMods]]);
|
|
274
|
+
return await totems.write.create([
|
|
275
|
+
details ? Object.assign({
|
|
276
|
+
ticker,
|
|
277
|
+
decimals,
|
|
278
|
+
}, details) : totemDetails(ticker, decimals),
|
|
279
|
+
allocations.map(a => ({
|
|
280
|
+
...a,
|
|
281
|
+
label: a.label || "",
|
|
282
|
+
isMinter: a.hasOwnProperty('isMinter') ? a.isMinter : false,
|
|
283
|
+
})),
|
|
284
|
+
_mods,
|
|
285
|
+
referrer,
|
|
286
|
+
], { account: creator, value: baseFee + modsFee });
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export const transfer = async (
|
|
290
|
+
totems:any,
|
|
291
|
+
ticker:string,
|
|
292
|
+
from:string,
|
|
293
|
+
to:string,
|
|
294
|
+
amount:number|bigint,
|
|
295
|
+
memo:string = "",
|
|
296
|
+
) => {
|
|
297
|
+
return await totems.write.transfer([
|
|
298
|
+
ticker,
|
|
299
|
+
from,
|
|
300
|
+
to,
|
|
301
|
+
amount,
|
|
302
|
+
memo,
|
|
303
|
+
], { account: from });
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export const mint = async (
|
|
307
|
+
totems:any,
|
|
308
|
+
mod:string,
|
|
309
|
+
minter:string,
|
|
310
|
+
ticker:string,
|
|
311
|
+
amount:number|bigint,
|
|
312
|
+
memo:string = "",
|
|
313
|
+
payment:number|bigint = 0n,
|
|
314
|
+
) => {
|
|
315
|
+
return await totems.write.mint([
|
|
316
|
+
mod,
|
|
317
|
+
minter,
|
|
318
|
+
ticker,
|
|
319
|
+
amount,
|
|
320
|
+
memo,
|
|
321
|
+
], { account: minter, value: payment });
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export const burn = async (
|
|
325
|
+
totems:any,
|
|
326
|
+
ticker:string,
|
|
327
|
+
owner:string,
|
|
328
|
+
amount:number|bigint,
|
|
329
|
+
memo:string = "",
|
|
330
|
+
) => {
|
|
331
|
+
return await totems.write.burn([
|
|
332
|
+
ticker,
|
|
333
|
+
owner,
|
|
334
|
+
amount,
|
|
335
|
+
memo,
|
|
336
|
+
], { account: owner });
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export const getBalance = async (
|
|
340
|
+
totems:any,
|
|
341
|
+
ticker:string,
|
|
342
|
+
account:string,
|
|
343
|
+
) => {
|
|
344
|
+
return await totems.read.getBalance([ticker, account]);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export const getTotem = async (
|
|
348
|
+
totems:any,
|
|
349
|
+
ticker:string,
|
|
350
|
+
) => {
|
|
351
|
+
return await totems.read.getTotem([ticker]);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export const getTotems = async (
|
|
355
|
+
totems:any,
|
|
356
|
+
tickers:string[],
|
|
357
|
+
) => {
|
|
358
|
+
return await totems.read.getTotems([tickers]);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export const getStats = async (
|
|
362
|
+
totems:any,
|
|
363
|
+
ticker:string,
|
|
364
|
+
) => {
|
|
365
|
+
return await totems.read.getStats([ticker]);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export const transferOwnership = async (
|
|
369
|
+
totems:any,
|
|
370
|
+
ticker:string,
|
|
371
|
+
currentOwner:string,
|
|
372
|
+
newOwner:string,
|
|
373
|
+
) => {
|
|
374
|
+
return await totems.write.transferOwnership([
|
|
375
|
+
ticker,
|
|
376
|
+
newOwner,
|
|
377
|
+
], { account: currentOwner });
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export const getMod = async (
|
|
381
|
+
market:any,
|
|
382
|
+
mod:string,
|
|
383
|
+
) => {
|
|
384
|
+
return await market.read.getMod([mod]);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
export const getMods = async (
|
|
388
|
+
market:any,
|
|
389
|
+
mods:string[],
|
|
390
|
+
) => {
|
|
391
|
+
return await market.read.getMods([mods]);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export const getModFee = async (
|
|
395
|
+
market:any,
|
|
396
|
+
mod:string,
|
|
397
|
+
) => {
|
|
398
|
+
return await market.read.getModFee([mod]);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export const getModsFee = async (
|
|
402
|
+
market:any,
|
|
403
|
+
mods:string[],
|
|
404
|
+
) => {
|
|
405
|
+
return await market.read.getModsFee([mods]);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export const isLicensed = async (
|
|
409
|
+
totems:any,
|
|
410
|
+
ticker:string,
|
|
411
|
+
mod:string,
|
|
412
|
+
) => {
|
|
413
|
+
return await totems.read.isLicensed([ticker, mod]);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export const getRelays = async (
|
|
417
|
+
totems:any,
|
|
418
|
+
ticker:string,
|
|
419
|
+
) => {
|
|
420
|
+
return await totems.read.getRelays([ticker]);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export const getSupportedHooks = async (
|
|
424
|
+
market:any,
|
|
425
|
+
mod:string,
|
|
426
|
+
) => {
|
|
427
|
+
return await market.read.getSupportedHooks([mod]);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export const isUnlimitedMinter = async (
|
|
431
|
+
market:any,
|
|
432
|
+
mod:string,
|
|
433
|
+
) => {
|
|
434
|
+
return await market.read.isUnlimitedMinter([mod]);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
export const addMod = async (
|
|
438
|
+
proxyMod:any,
|
|
439
|
+
totems:any,
|
|
440
|
+
market:any,
|
|
441
|
+
ticker:string,
|
|
442
|
+
hooks:number[],
|
|
443
|
+
mod:string,
|
|
444
|
+
caller:string,
|
|
445
|
+
referrer:string = ZERO_ADDRESS,
|
|
446
|
+
) => {
|
|
447
|
+
const modFee = await market.read.getModFee([mod]);
|
|
448
|
+
const referrerFee = await totems.read.getFee([referrer]);
|
|
449
|
+
return await proxyMod.write.addMod([
|
|
450
|
+
ticker,
|
|
451
|
+
hooks,
|
|
452
|
+
mod,
|
|
453
|
+
referrer,
|
|
454
|
+
], { account: caller, value: modFee + referrerFee });
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
export const removeMod = async (
|
|
458
|
+
proxyMod:any,
|
|
459
|
+
ticker:string,
|
|
460
|
+
mod:string,
|
|
461
|
+
caller:string,
|
|
462
|
+
) => {
|
|
463
|
+
return await proxyMod.write.removeMod([
|
|
464
|
+
ticker,
|
|
465
|
+
mod,
|
|
466
|
+
], { account: caller });
|
|
467
|
+
}
|
package/test/constants.d.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
export declare const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
|
|
2
|
-
export declare const MIN_BASE_FEE = 500000000000000n;
|
|
3
|
-
export declare const BURNED_FEE = 100000000000000n;
|
|
4
|
-
export declare const Hook: {
|
|
5
|
-
readonly Created: 0;
|
|
6
|
-
readonly Mint: 1;
|
|
7
|
-
readonly Burn: 2;
|
|
8
|
-
readonly Transfer: 3;
|
|
9
|
-
readonly TransferOwnership: 4;
|
|
10
|
-
};
|
|
11
|
-
export type HookType = typeof Hook[keyof typeof Hook];
|
package/test/constants.js
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
// test/constants.ts
|
|
2
|
-
var ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
|
|
3
|
-
var MIN_BASE_FEE = 500000000000000n;
|
|
4
|
-
var BURNED_FEE = 100000000000000n;
|
|
5
|
-
var Hook = {
|
|
6
|
-
Created: 0,
|
|
7
|
-
Mint: 1,
|
|
8
|
-
Burn: 2,
|
|
9
|
-
Transfer: 3,
|
|
10
|
-
TransferOwnership: 4
|
|
11
|
-
};
|
|
12
|
-
export {
|
|
13
|
-
ZERO_ADDRESS,
|
|
14
|
-
MIN_BASE_FEE,
|
|
15
|
-
Hook,
|
|
16
|
-
BURNED_FEE
|
|
17
|
-
};
|
package/test/helpers.js
DELETED
|
@@ -1,300 +0,0 @@
|
|
|
1
|
-
// test/helpers.ts
|
|
2
|
-
import {network} from "hardhat";
|
|
3
|
-
import {keccak256, toBytes, decodeErrorResult} from "viem";
|
|
4
|
-
|
|
5
|
-
// test/constants.ts
|
|
6
|
-
var ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
|
|
7
|
-
var MIN_BASE_FEE = 500000000000000n;
|
|
8
|
-
var BURNED_FEE = 100000000000000n;
|
|
9
|
-
var Hook = {
|
|
10
|
-
Created: 0,
|
|
11
|
-
Mint: 1,
|
|
12
|
-
Burn: 2,
|
|
13
|
-
Transfer: 3,
|
|
14
|
-
TransferOwnership: 4
|
|
15
|
-
};
|
|
16
|
-
// test/helpers.ts
|
|
17
|
-
function errorSelector(signature) {
|
|
18
|
-
return keccak256(toBytes(signature)).slice(0, 10);
|
|
19
|
-
}
|
|
20
|
-
var getErrorData = function(error) {
|
|
21
|
-
const data = error?.cause?.cause?.data || error?.cause?.data || error?.data || error?.message?.match(/return data: (0x[a-fA-F0-9]+)/)?.[1] || error?.message?.match(/data: (0x[a-fA-F0-9]+)/)?.[1];
|
|
22
|
-
return data || null;
|
|
23
|
-
};
|
|
24
|
-
async function expectCustomError(promise, expectedSelector, errorName) {
|
|
25
|
-
try {
|
|
26
|
-
await promise;
|
|
27
|
-
throw new Error(`Expected ${errorName} but transaction succeeded`);
|
|
28
|
-
} catch (e) {
|
|
29
|
-
if (e.message?.startsWith(`Expected ${errorName}`))
|
|
30
|
-
throw e;
|
|
31
|
-
const data = getErrorData(e);
|
|
32
|
-
if (!data) {
|
|
33
|
-
throw new Error(`Expected ${errorName} but got error without revert data: ${e.message}`);
|
|
34
|
-
}
|
|
35
|
-
const actualSelector = data.slice(0, 10).toLowerCase();
|
|
36
|
-
const expected = expectedSelector.toLowerCase();
|
|
37
|
-
if (actualSelector !== expected) {
|
|
38
|
-
throw new Error(`Expected ${errorName} (${expected}) but got selector ${actualSelector}\nFull data: ${data}`);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
async function expectRevertMessage(promise, expectedMessage) {
|
|
43
|
-
const ERROR_STRING_SELECTOR = "0x08c379a0";
|
|
44
|
-
try {
|
|
45
|
-
await promise;
|
|
46
|
-
throw new Error(`Expected revert with "${expectedMessage}" but transaction succeeded`);
|
|
47
|
-
} catch (e) {
|
|
48
|
-
if (e.message?.startsWith("Expected revert"))
|
|
49
|
-
throw e;
|
|
50
|
-
const data = getErrorData(e);
|
|
51
|
-
if (!data) {
|
|
52
|
-
const matches = typeof expectedMessage === "string" ? e.message?.includes(expectedMessage) : expectedMessage.test(e.message);
|
|
53
|
-
if (!matches) {
|
|
54
|
-
throw new Error(`Expected revert with "${expectedMessage}" but got: ${e.message}`);
|
|
55
|
-
}
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
const selector = data.slice(0, 10).toLowerCase();
|
|
59
|
-
if (selector !== ERROR_STRING_SELECTOR) {
|
|
60
|
-
const matches = typeof expectedMessage === "string" ? e.message?.includes(expectedMessage) : expectedMessage.test(e.message);
|
|
61
|
-
if (!matches) {
|
|
62
|
-
throw new Error(`Expected string revert but got custom error with selector ${selector}`);
|
|
63
|
-
}
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
try {
|
|
67
|
-
const abi = [{
|
|
68
|
-
type: "error",
|
|
69
|
-
name: "Error",
|
|
70
|
-
inputs: [{ name: "message", type: "string" }]
|
|
71
|
-
}];
|
|
72
|
-
const decoded = decodeErrorResult({ abi, data });
|
|
73
|
-
const message = decoded.args[0];
|
|
74
|
-
const matches = typeof expectedMessage === "string" ? message.includes(expectedMessage) : expectedMessage.test(message);
|
|
75
|
-
if (!matches) {
|
|
76
|
-
throw new Error(`Expected revert with "${expectedMessage}" but got "${message}"`);
|
|
77
|
-
}
|
|
78
|
-
} catch (decodeError) {
|
|
79
|
-
const matches = typeof expectedMessage === "string" ? e.message?.includes(expectedMessage) : expectedMessage.test(e.message);
|
|
80
|
-
if (!matches) {
|
|
81
|
-
throw new Error(`Expected revert with "${expectedMessage}" but decoding failed: ${e.message}`);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
var ErrorSelectors = {
|
|
87
|
-
InvalidModEventOrigin: errorSelector("InvalidModEventOrigin()"),
|
|
88
|
-
NotLicensed: errorSelector("NotLicensed()"),
|
|
89
|
-
Unauthorized: errorSelector("Unauthorized()"),
|
|
90
|
-
TotemNotFound: errorSelector("TotemNotFound(string)"),
|
|
91
|
-
TotemNotActive: errorSelector("TotemNotActive()"),
|
|
92
|
-
InsufficientBalance: errorSelector("InsufficientBalance(uint256,uint256)"),
|
|
93
|
-
CantSetLicense: errorSelector("CantSetLicense()")
|
|
94
|
-
};
|
|
95
|
-
var setupTotemsTest = async (minBaseFee = MIN_BASE_FEE, burnedFee = BURNED_FEE) => {
|
|
96
|
-
const { viem } = await network.connect();
|
|
97
|
-
const publicClient = await viem.getPublicClient();
|
|
98
|
-
const walletClient = await viem.getWalletClient();
|
|
99
|
-
const addresses = await walletClient.getAddresses();
|
|
100
|
-
const proxyModInitializer = addresses[0];
|
|
101
|
-
const proxyMod = await viem.deployContract("ProxyMod", [
|
|
102
|
-
proxyModInitializer
|
|
103
|
-
]);
|
|
104
|
-
let market = await viem.deployContract("ModMarket", [minBaseFee, burnedFee]);
|
|
105
|
-
let totems = await viem.deployContract("Totems", [
|
|
106
|
-
market.address,
|
|
107
|
-
proxyMod.address,
|
|
108
|
-
minBaseFee,
|
|
109
|
-
burnedFee
|
|
110
|
-
]);
|
|
111
|
-
totems = await viem.getContractAt("ITotems", totems.address);
|
|
112
|
-
market = await viem.getContractAt("IMarket", market.address);
|
|
113
|
-
await proxyMod.write.initialize([totems.address, market.address], { account: proxyModInitializer });
|
|
114
|
-
return {
|
|
115
|
-
viem,
|
|
116
|
-
publicClient,
|
|
117
|
-
market,
|
|
118
|
-
totems,
|
|
119
|
-
accounts: addresses.slice(0, addresses.length),
|
|
120
|
-
proxyModSeller: addresses[0],
|
|
121
|
-
proxyMod
|
|
122
|
-
};
|
|
123
|
-
};
|
|
124
|
-
var modDetails = (details) => Object.assign({
|
|
125
|
-
name: "Test Mod",
|
|
126
|
-
summary: "A test mod",
|
|
127
|
-
markdown: "## Test Mod\nThis is a test mod.",
|
|
128
|
-
image: "https://example.com/image.png",
|
|
129
|
-
website: "https://example.com",
|
|
130
|
-
websiteTickerPath: "/path/to/{ticker}",
|
|
131
|
-
isMinter: false,
|
|
132
|
-
needsUnlimited: false
|
|
133
|
-
}, details || {});
|
|
134
|
-
var publishMod = async (market, seller, contract, hooks = [], details = modDetails(), requiredActions = [], referrer = ZERO_ADDRESS, price = 1000000n, fee = undefined) => {
|
|
135
|
-
fee = fee ?? await market.read.getFee([referrer]);
|
|
136
|
-
return market.write.publish([
|
|
137
|
-
contract,
|
|
138
|
-
hooks,
|
|
139
|
-
price,
|
|
140
|
-
details,
|
|
141
|
-
requiredActions,
|
|
142
|
-
referrer
|
|
143
|
-
], { value: fee, account: seller });
|
|
144
|
-
};
|
|
145
|
-
var totemDetails = (ticker, decimals) => {
|
|
146
|
-
return {
|
|
147
|
-
ticker,
|
|
148
|
-
decimals,
|
|
149
|
-
name: `${ticker} Totem`,
|
|
150
|
-
description: `This is the ${ticker} totem.`,
|
|
151
|
-
image: `https://example.com/${ticker.toLowerCase()}.png`,
|
|
152
|
-
website: `https://example.com/${ticker.toLowerCase()}`,
|
|
153
|
-
seed: "0x1110762033e7a10db4502359a19a61eb81312834769b8419047a2c9ae03ee847"
|
|
154
|
-
};
|
|
155
|
-
};
|
|
156
|
-
var createTotem = async (totems, market, creator, ticker, decimals, allocations, mods, referrer = ZERO_ADDRESS, details = undefined) => {
|
|
157
|
-
const baseFee = await totems.read.getFee([referrer]);
|
|
158
|
-
const _mods = Object.assign({
|
|
159
|
-
transfer: [],
|
|
160
|
-
mint: [],
|
|
161
|
-
burn: [],
|
|
162
|
-
created: [],
|
|
163
|
-
transferOwnership: []
|
|
164
|
-
}, mods || {});
|
|
165
|
-
const uniqueMods = new Set;
|
|
166
|
-
Object.values(_mods).forEach((modList) => {
|
|
167
|
-
modList.forEach((m) => uniqueMods.add(m));
|
|
168
|
-
});
|
|
169
|
-
const modsFee = await market.read.getModsFee([[...uniqueMods]]);
|
|
170
|
-
return await totems.write.create([
|
|
171
|
-
details ? Object.assign({
|
|
172
|
-
ticker,
|
|
173
|
-
decimals
|
|
174
|
-
}, details) : totemDetails(ticker, decimals),
|
|
175
|
-
allocations.map((a) => ({
|
|
176
|
-
...a,
|
|
177
|
-
label: a.label || "",
|
|
178
|
-
isMinter: a.hasOwnProperty("isMinter") ? a.isMinter : false
|
|
179
|
-
})),
|
|
180
|
-
_mods,
|
|
181
|
-
referrer
|
|
182
|
-
], { account: creator, value: baseFee + modsFee });
|
|
183
|
-
};
|
|
184
|
-
var transfer = async (totems, ticker, from, to, amount, memo = "") => {
|
|
185
|
-
return await totems.write.transfer([
|
|
186
|
-
ticker,
|
|
187
|
-
from,
|
|
188
|
-
to,
|
|
189
|
-
amount,
|
|
190
|
-
memo
|
|
191
|
-
], { account: from });
|
|
192
|
-
};
|
|
193
|
-
var mint = async (totems, mod, minter, ticker, amount, memo = "", payment = 0n) => {
|
|
194
|
-
return await totems.write.mint([
|
|
195
|
-
mod,
|
|
196
|
-
minter,
|
|
197
|
-
ticker,
|
|
198
|
-
amount,
|
|
199
|
-
memo
|
|
200
|
-
], { account: minter, value: payment });
|
|
201
|
-
};
|
|
202
|
-
var burn = async (totems, ticker, owner, amount, memo = "") => {
|
|
203
|
-
return await totems.write.burn([
|
|
204
|
-
ticker,
|
|
205
|
-
owner,
|
|
206
|
-
amount,
|
|
207
|
-
memo
|
|
208
|
-
], { account: owner });
|
|
209
|
-
};
|
|
210
|
-
var getBalance = async (totems, ticker, account) => {
|
|
211
|
-
return await totems.read.getBalance([ticker, account]);
|
|
212
|
-
};
|
|
213
|
-
var getTotem = async (totems, ticker) => {
|
|
214
|
-
return await totems.read.getTotem([ticker]);
|
|
215
|
-
};
|
|
216
|
-
var getTotems = async (totems, tickers) => {
|
|
217
|
-
return await totems.read.getTotems([tickers]);
|
|
218
|
-
};
|
|
219
|
-
var getStats = async (totems, ticker) => {
|
|
220
|
-
return await totems.read.getStats([ticker]);
|
|
221
|
-
};
|
|
222
|
-
var transferOwnership = async (totems, ticker, currentOwner, newOwner) => {
|
|
223
|
-
return await totems.write.transferOwnership([
|
|
224
|
-
ticker,
|
|
225
|
-
newOwner
|
|
226
|
-
], { account: currentOwner });
|
|
227
|
-
};
|
|
228
|
-
var getMod = async (market, mod) => {
|
|
229
|
-
return await market.read.getMod([mod]);
|
|
230
|
-
};
|
|
231
|
-
var getMods = async (market, mods) => {
|
|
232
|
-
return await market.read.getMods([mods]);
|
|
233
|
-
};
|
|
234
|
-
var getModFee = async (market, mod) => {
|
|
235
|
-
return await market.read.getModFee([mod]);
|
|
236
|
-
};
|
|
237
|
-
var getModsFee = async (market, mods) => {
|
|
238
|
-
return await market.read.getModsFee([mods]);
|
|
239
|
-
};
|
|
240
|
-
var isLicensed = async (totems, ticker, mod) => {
|
|
241
|
-
return await totems.read.isLicensed([ticker, mod]);
|
|
242
|
-
};
|
|
243
|
-
var getRelays = async (totems, ticker) => {
|
|
244
|
-
return await totems.read.getRelays([ticker]);
|
|
245
|
-
};
|
|
246
|
-
var getSupportedHooks = async (market, mod) => {
|
|
247
|
-
return await market.read.getSupportedHooks([mod]);
|
|
248
|
-
};
|
|
249
|
-
var isUnlimitedMinter = async (market, mod) => {
|
|
250
|
-
return await market.read.isUnlimitedMinter([mod]);
|
|
251
|
-
};
|
|
252
|
-
var addMod = async (proxyMod, totems, market, ticker, hooks, mod, caller, referrer = ZERO_ADDRESS) => {
|
|
253
|
-
const modFee = await market.read.getModFee([mod]);
|
|
254
|
-
const referrerFee = await totems.read.getFee([referrer]);
|
|
255
|
-
return await proxyMod.write.addMod([
|
|
256
|
-
ticker,
|
|
257
|
-
hooks,
|
|
258
|
-
mod,
|
|
259
|
-
referrer
|
|
260
|
-
], { account: caller, value: modFee + referrerFee });
|
|
261
|
-
};
|
|
262
|
-
var removeMod = async (proxyMod, ticker, mod, caller) => {
|
|
263
|
-
return await proxyMod.write.removeMod([
|
|
264
|
-
ticker,
|
|
265
|
-
mod
|
|
266
|
-
], { account: caller });
|
|
267
|
-
};
|
|
268
|
-
export {
|
|
269
|
-
transferOwnership,
|
|
270
|
-
transfer,
|
|
271
|
-
totemDetails,
|
|
272
|
-
setupTotemsTest,
|
|
273
|
-
removeMod,
|
|
274
|
-
publishMod,
|
|
275
|
-
modDetails,
|
|
276
|
-
mint,
|
|
277
|
-
isUnlimitedMinter,
|
|
278
|
-
isLicensed,
|
|
279
|
-
getTotems,
|
|
280
|
-
getTotem,
|
|
281
|
-
getSupportedHooks,
|
|
282
|
-
getStats,
|
|
283
|
-
getRelays,
|
|
284
|
-
getModsFee,
|
|
285
|
-
getMods,
|
|
286
|
-
getModFee,
|
|
287
|
-
getMod,
|
|
288
|
-
getBalance,
|
|
289
|
-
expectRevertMessage,
|
|
290
|
-
expectCustomError,
|
|
291
|
-
errorSelector,
|
|
292
|
-
createTotem,
|
|
293
|
-
burn,
|
|
294
|
-
addMod,
|
|
295
|
-
ZERO_ADDRESS,
|
|
296
|
-
MIN_BASE_FEE,
|
|
297
|
-
Hook,
|
|
298
|
-
ErrorSelectors,
|
|
299
|
-
BURNED_FEE
|
|
300
|
-
};
|