@totems/evm 1.0.7 → 1.0.9

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": "@totems/evm",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "type": "module",
5
5
  "description": "Totems EVM smart contracts for building modular token systems",
6
6
  "author": "nsjames",
@@ -24,14 +24,13 @@
24
24
  "contracts/**/*.sol",
25
25
  "interfaces/**/*.sol",
26
26
  "mods/**/*.sol",
27
- "test/**/*"
27
+ "test/**/*",
28
+ "artifacts/**/*"
28
29
  ],
29
30
  "exports": {
30
31
  "./test/helpers": {
31
32
  "types": "./test/helpers.d.ts",
32
- "import": "./test/helpers.js",
33
- "require": "./test/helpers.js",
34
- "default": "./test/helpers.js"
33
+ "default": "./test/helpers.ts"
35
34
  },
36
35
  "./contracts/*": "./contracts/*",
37
36
  "./interfaces/*": "./interfaces/*",
package/test/helpers.d.ts CHANGED
@@ -37,8 +37,14 @@ export declare const Hook: {
37
37
  export declare const setupTotemsTest: (minBaseFee?: bigint, burnedFee?: bigint) => Promise<{
38
38
  viem: any;
39
39
  publicClient: any;
40
- market: any;
41
- totems: any;
40
+ market: {
41
+ address: any;
42
+ abi: any;
43
+ };
44
+ totems: {
45
+ address: any;
46
+ abi: any;
47
+ };
42
48
  accounts: any;
43
49
  proxyModSeller: any;
44
50
  proxyMod: any;
@@ -0,0 +1,525 @@
1
+ import {network} from "hardhat";
2
+ import { keccak256, toBytes, decodeErrorResult, Abi, getContract } from "viem";
3
+ import { readFileSync, existsSync } from "fs";
4
+ import { dirname, join } from "path";
5
+ import { fileURLToPath } from "url";
6
+
7
+ // Detect if we're running from the package or the main project
8
+ const __dirname = dirname(fileURLToPath(import.meta.url));
9
+ const packageArtifactsDir = join(__dirname, '..', 'artifacts');
10
+ const isPackage = existsSync(join(packageArtifactsDir, 'ProxyMod.json'));
11
+
12
+ function loadArtifact(name: string) {
13
+ if (isPackage) {
14
+ // Load from package artifacts
15
+ const path = join(packageArtifactsDir, `${name}.json`);
16
+ return JSON.parse(readFileSync(path, 'utf-8'));
17
+ }
18
+ // Load from main project's hardhat artifacts
19
+ const paths: Record<string, string> = {
20
+ 'ProxyMod': 'artifacts/contracts/mods/ProxyMod.sol/ProxyMod.json',
21
+ 'ModMarket': 'artifacts/contracts/market/ModMarket.sol/ModMarket.json',
22
+ 'Totems': 'artifacts/contracts/totems/Totems.sol/Totems.json',
23
+ 'ITotems': 'artifacts/contracts/interfaces/ITotems.sol/ITotems.json',
24
+ 'IMarket': 'artifacts/contracts/interfaces/IMarket.sol/IMarket.json',
25
+ };
26
+ const path = join(__dirname, '..', paths[name]);
27
+ return JSON.parse(readFileSync(path, 'utf-8'));
28
+ }
29
+
30
+ const ProxyModArtifact = loadArtifact('ProxyMod');
31
+ const ModMarketArtifact = loadArtifact('ModMarket');
32
+ const TotemsArtifact = loadArtifact('Totems');
33
+ const ITotemsArtifact = loadArtifact('ITotems');
34
+ const IMarketArtifact = loadArtifact('IMarket');
35
+
36
+ export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
37
+
38
+ /**
39
+ * Computes the 4-byte selector for a custom error signature
40
+ * @param signature Error signature like "NotLicensed()" or "InsufficientBalance(uint256,uint256)"
41
+ */
42
+ export function errorSelector(signature: string): string {
43
+ return keccak256(toBytes(signature)).slice(0, 10); // 0x + 8 hex chars = 4 bytes
44
+ }
45
+
46
+ /**
47
+ * Extracts the error selector from a caught error's revert data
48
+ */
49
+ function getErrorData(error: any): string | null {
50
+ // Try common paths where revert data might be
51
+ const data = error?.cause?.cause?.data
52
+ || error?.cause?.data
53
+ || error?.data
54
+ || error?.message?.match(/return data: (0x[a-fA-F0-9]+)/)?.[1]
55
+ || error?.message?.match(/data: (0x[a-fA-F0-9]+)/)?.[1];
56
+ return data || null;
57
+ }
58
+
59
+ /**
60
+ * Asserts that a promise rejects with a specific custom error
61
+ * @param promise The promise to test
62
+ * @param expectedSelector The expected error selector (use errorSelector() to compute)
63
+ * @param errorName Human-readable error name for assertion messages
64
+ */
65
+ export async function expectCustomError(
66
+ promise: Promise<any>,
67
+ expectedSelector: string,
68
+ errorName: string
69
+ ): Promise<void> {
70
+ try {
71
+ await promise;
72
+ throw new Error(`Expected ${errorName} but transaction succeeded`);
73
+ } catch (e: any) {
74
+ if (e.message?.startsWith(`Expected ${errorName}`)) throw e;
75
+
76
+ const data = getErrorData(e);
77
+ if (!data) {
78
+ throw new Error(`Expected ${errorName} but got error without revert data: ${e.message}`);
79
+ }
80
+
81
+ const actualSelector = data.slice(0, 10).toLowerCase();
82
+ const expected = expectedSelector.toLowerCase();
83
+
84
+ if (actualSelector !== expected) {
85
+ throw new Error(
86
+ `Expected ${errorName} (${expected}) but got selector ${actualSelector}\nFull data: ${data}`
87
+ );
88
+ }
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Asserts that a promise rejects with a string revert message
94
+ * Error(string) selector is 0x08c379a0
95
+ */
96
+ export async function expectRevertMessage(
97
+ promise: Promise<any>,
98
+ expectedMessage: string | RegExp
99
+ ): Promise<void> {
100
+ const ERROR_STRING_SELECTOR = "0x08c379a0";
101
+
102
+ try {
103
+ await promise;
104
+ throw new Error(`Expected revert with "${expectedMessage}" but transaction succeeded`);
105
+ } catch (e: any) {
106
+ if (e.message?.startsWith("Expected revert")) throw e;
107
+
108
+ const data = getErrorData(e);
109
+ if (!data) {
110
+ // Fallback to checking error message directly
111
+ const matches = typeof expectedMessage === 'string'
112
+ ? e.message?.includes(expectedMessage)
113
+ : expectedMessage.test(e.message);
114
+ if (!matches) {
115
+ throw new Error(`Expected revert with "${expectedMessage}" but got: ${e.message}`);
116
+ }
117
+ return;
118
+ }
119
+
120
+ const selector = data.slice(0, 10).toLowerCase();
121
+ if (selector !== ERROR_STRING_SELECTOR) {
122
+ // Not a string error, check if message is in the raw error
123
+ const matches = typeof expectedMessage === 'string'
124
+ ? e.message?.includes(expectedMessage)
125
+ : expectedMessage.test(e.message);
126
+ if (!matches) {
127
+ throw new Error(`Expected string revert but got custom error with selector ${selector}`);
128
+ }
129
+ return;
130
+ }
131
+
132
+ // Decode the string from the ABI-encoded data
133
+ // Format: selector (4 bytes) + offset (32 bytes) + length (32 bytes) + string data
134
+ try {
135
+ const abi: Abi = [{
136
+ type: 'error',
137
+ name: 'Error',
138
+ inputs: [{ name: 'message', type: 'string' }]
139
+ }];
140
+ const decoded = decodeErrorResult({ abi, data: data as `0x${string}` });
141
+ const message = (decoded.args as string[])[0];
142
+
143
+ const matches = typeof expectedMessage === 'string'
144
+ ? message.includes(expectedMessage)
145
+ : expectedMessage.test(message);
146
+
147
+ if (!matches) {
148
+ throw new Error(`Expected revert with "${expectedMessage}" but got "${message}"`);
149
+ }
150
+ } catch (decodeError) {
151
+ // If decoding fails, fall back to checking error message
152
+ const matches = typeof expectedMessage === 'string'
153
+ ? e.message?.includes(expectedMessage)
154
+ : expectedMessage.test(e.message);
155
+ if (!matches) {
156
+ throw new Error(`Expected revert with "${expectedMessage}" but decoding failed: ${e.message}`);
157
+ }
158
+ }
159
+ }
160
+ }
161
+
162
+ // Pre-computed selectors for common errors
163
+ export const ErrorSelectors = {
164
+ // TotemMod errors
165
+ InvalidModEventOrigin: errorSelector("InvalidModEventOrigin()"),
166
+ NotLicensed: errorSelector("NotLicensed()"),
167
+
168
+ // Totems errors
169
+ Unauthorized: errorSelector("Unauthorized()"),
170
+ TotemNotFound: errorSelector("TotemNotFound(string)"),
171
+ TotemNotActive: errorSelector("TotemNotActive()"),
172
+ InsufficientBalance: errorSelector("InsufficientBalance(uint256,uint256)"),
173
+ CantSetLicense: errorSelector("CantSetLicense()"),
174
+ };
175
+
176
+ export const MIN_BASE_FEE = 500000000000000n; // 0.0005 ether
177
+ export const BURNED_FEE = 100000000000000n; // 0.0001 ether
178
+
179
+ export const Hook = {
180
+ Created: 0,
181
+ Mint: 1,
182
+ Burn: 2,
183
+ Transfer: 3,
184
+ TransferOwnership: 4,
185
+ } as const;
186
+
187
+ export const setupTotemsTest = async (minBaseFee: bigint = MIN_BASE_FEE, burnedFee: bigint = BURNED_FEE) => {
188
+ const { viem } = await network.connect() as any;
189
+ const publicClient = await viem.getPublicClient();
190
+ const walletClient = await viem.getWalletClient();
191
+
192
+ const addresses = await walletClient.getAddresses();
193
+ const proxyModInitializer = addresses[0];
194
+
195
+ // Deploy using pre-built artifacts from the package
196
+ const proxyModHash = await walletClient.deployContract({
197
+ abi: ProxyModArtifact.abi,
198
+ bytecode: ProxyModArtifact.bytecode,
199
+ args: [proxyModInitializer],
200
+ account: proxyModInitializer,
201
+ });
202
+ const proxyModReceipt = await publicClient.waitForTransactionReceipt({ hash: proxyModHash });
203
+ const proxyMod = getContract({
204
+ address: proxyModReceipt.contractAddress!,
205
+ abi: ProxyModArtifact.abi,
206
+ client: { public: publicClient, wallet: walletClient },
207
+ }) as any;
208
+
209
+ const marketHash = await walletClient.deployContract({
210
+ abi: ModMarketArtifact.abi,
211
+ bytecode: ModMarketArtifact.bytecode,
212
+ args: [minBaseFee, burnedFee],
213
+ account: proxyModInitializer,
214
+ });
215
+ const marketReceipt = await publicClient.waitForTransactionReceipt({ hash: marketHash });
216
+
217
+ const totemsHash = await walletClient.deployContract({
218
+ abi: TotemsArtifact.abi,
219
+ bytecode: TotemsArtifact.bytecode,
220
+ args: [marketReceipt.contractAddress!, proxyModReceipt.contractAddress!, minBaseFee, burnedFee],
221
+ account: proxyModInitializer,
222
+ });
223
+ const totemsReceipt = await publicClient.waitForTransactionReceipt({ hash: totemsHash });
224
+
225
+ // Use interface ABIs for the contract instances
226
+ const totems = getContract({
227
+ address: totemsReceipt.contractAddress!,
228
+ abi: ITotemsArtifact.abi,
229
+ client: { public: publicClient, wallet: walletClient },
230
+ });
231
+
232
+ const market = getContract({
233
+ address: marketReceipt.contractAddress!,
234
+ abi: IMarketArtifact.abi,
235
+ client: { public: publicClient, wallet: walletClient },
236
+ });
237
+
238
+ // Initialize proxy mod
239
+ await proxyMod.write.initialize([totemsReceipt.contractAddress!, marketReceipt.contractAddress!], { account: proxyModInitializer });
240
+
241
+ return {
242
+ viem,
243
+ publicClient,
244
+ market,
245
+ totems,
246
+ accounts: addresses.slice(0, addresses.length),
247
+ proxyModSeller: addresses[0],
248
+ proxyMod,
249
+ }
250
+
251
+ }
252
+
253
+
254
+ export const modDetails = (details?:any) => Object.assign({
255
+ name: "Test Mod",
256
+ summary: "A test mod",
257
+ markdown: "## Test Mod\nThis is a test mod.",
258
+ image: "https://example.com/image.png",
259
+ website: "https://example.com",
260
+ websiteTickerPath: "/path/to/{ticker}",
261
+ isMinter: false,
262
+ needsUnlimited: false,
263
+ }, details || {});
264
+
265
+ export const publishMod = async (
266
+ market:any,
267
+ seller:string,
268
+ contract:string,
269
+ hooks:number[] = [],
270
+ details = modDetails(),
271
+ requiredActions:any[] = [],
272
+ referrer = ZERO_ADDRESS,
273
+ price = 1_000_000n,
274
+ fee = undefined
275
+ ) => {
276
+ fee = fee ?? await market.read.getFee([referrer]);
277
+
278
+ return market.write.publish([
279
+ contract,
280
+ hooks,
281
+ price,
282
+ details,
283
+ requiredActions,
284
+ referrer,
285
+ ], { value: fee, account: seller });
286
+ }
287
+
288
+ export const totemDetails = (ticker:string, decimals:number) => {
289
+ return {
290
+ ticker: ticker,
291
+ decimals: decimals,
292
+ name: `${ticker} Totem`,
293
+ description: `This is the ${ticker} totem.`,
294
+ image: `https://example.com/${ticker.toLowerCase()}.png`,
295
+ website: `https://example.com/${ticker.toLowerCase()}`,
296
+ seed: '0x1110762033e7a10db4502359a19a61eb81312834769b8419047a2c9ae03ee847',
297
+ };
298
+ }
299
+
300
+ export const createTotem = async (
301
+ totems:any,
302
+ market:any,
303
+ creator:string,
304
+ ticker:string,
305
+ decimals:number,
306
+ allocations:any[],
307
+ mods?:{
308
+ transfer?:string[],
309
+ mint?:string[],
310
+ burn?:string[],
311
+ created?:string[],
312
+ transferOwnership?:string[]
313
+ },
314
+ referrer:string = ZERO_ADDRESS,
315
+ details:any = undefined,
316
+ ) => {
317
+ const baseFee = await totems.read.getFee([referrer]);
318
+
319
+ const _mods = Object.assign({
320
+ transfer: [],
321
+ mint: [],
322
+ burn: [],
323
+ created: [],
324
+ transferOwnership: [],
325
+ }, mods || {});
326
+ const uniqueMods = new Set<string>();
327
+ Object.values(_mods).forEach((modList:any[]) => {
328
+ modList.forEach(m => uniqueMods.add(m));
329
+ });
330
+
331
+ const modsFee = await market.read.getModsFee([[...uniqueMods]]);
332
+ return await totems.write.create([
333
+ details ? Object.assign({
334
+ ticker,
335
+ decimals,
336
+ }, details) : totemDetails(ticker, decimals),
337
+ allocations.map(a => ({
338
+ ...a,
339
+ label: a.label || "",
340
+ isMinter: a.hasOwnProperty('isMinter') ? a.isMinter : false,
341
+ })),
342
+ _mods,
343
+ referrer,
344
+ ], { account: creator, value: baseFee + modsFee });
345
+ }
346
+
347
+ export const transfer = async (
348
+ totems:any,
349
+ ticker:string,
350
+ from:string,
351
+ to:string,
352
+ amount:number|bigint,
353
+ memo:string = "",
354
+ ) => {
355
+ return await totems.write.transfer([
356
+ ticker,
357
+ from,
358
+ to,
359
+ amount,
360
+ memo,
361
+ ], { account: from });
362
+ }
363
+
364
+ export const mint = async (
365
+ totems:any,
366
+ mod:string,
367
+ minter:string,
368
+ ticker:string,
369
+ amount:number|bigint,
370
+ memo:string = "",
371
+ payment:number|bigint = 0n,
372
+ ) => {
373
+ return await totems.write.mint([
374
+ mod,
375
+ minter,
376
+ ticker,
377
+ amount,
378
+ memo,
379
+ ], { account: minter, value: payment });
380
+ }
381
+
382
+ export const burn = async (
383
+ totems:any,
384
+ ticker:string,
385
+ owner:string,
386
+ amount:number|bigint,
387
+ memo:string = "",
388
+ ) => {
389
+ return await totems.write.burn([
390
+ ticker,
391
+ owner,
392
+ amount,
393
+ memo,
394
+ ], { account: owner });
395
+ }
396
+
397
+ export const getBalance = async (
398
+ totems:any,
399
+ ticker:string,
400
+ account:string,
401
+ ) => {
402
+ return await totems.read.getBalance([ticker, account]);
403
+ }
404
+
405
+ export const getTotem = async (
406
+ totems:any,
407
+ ticker:string,
408
+ ) => {
409
+ return await totems.read.getTotem([ticker]);
410
+ }
411
+
412
+ export const getTotems = async (
413
+ totems:any,
414
+ tickers:string[],
415
+ ) => {
416
+ return await totems.read.getTotems([tickers]);
417
+ }
418
+
419
+ export const getStats = async (
420
+ totems:any,
421
+ ticker:string,
422
+ ) => {
423
+ return await totems.read.getStats([ticker]);
424
+ }
425
+
426
+ export const transferOwnership = async (
427
+ totems:any,
428
+ ticker:string,
429
+ currentOwner:string,
430
+ newOwner:string,
431
+ ) => {
432
+ return await totems.write.transferOwnership([
433
+ ticker,
434
+ newOwner,
435
+ ], { account: currentOwner });
436
+ }
437
+
438
+ export const getMod = async (
439
+ market:any,
440
+ mod:string,
441
+ ) => {
442
+ return await market.read.getMod([mod]);
443
+ }
444
+
445
+ export const getMods = async (
446
+ market:any,
447
+ mods:string[],
448
+ ) => {
449
+ return await market.read.getMods([mods]);
450
+ }
451
+
452
+ export const getModFee = async (
453
+ market:any,
454
+ mod:string,
455
+ ) => {
456
+ return await market.read.getModFee([mod]);
457
+ }
458
+
459
+ export const getModsFee = async (
460
+ market:any,
461
+ mods:string[],
462
+ ) => {
463
+ return await market.read.getModsFee([mods]);
464
+ }
465
+
466
+ export const isLicensed = async (
467
+ totems:any,
468
+ ticker:string,
469
+ mod:string,
470
+ ) => {
471
+ return await totems.read.isLicensed([ticker, mod]);
472
+ }
473
+
474
+ export const getRelays = async (
475
+ totems:any,
476
+ ticker:string,
477
+ ) => {
478
+ return await totems.read.getRelays([ticker]);
479
+ }
480
+
481
+ export const getSupportedHooks = async (
482
+ market:any,
483
+ mod:string,
484
+ ) => {
485
+ return await market.read.getSupportedHooks([mod]);
486
+ }
487
+
488
+ export const isUnlimitedMinter = async (
489
+ market:any,
490
+ mod:string,
491
+ ) => {
492
+ return await market.read.isUnlimitedMinter([mod]);
493
+ }
494
+
495
+ export const addMod = async (
496
+ proxyMod:any,
497
+ totems:any,
498
+ market:any,
499
+ ticker:string,
500
+ hooks:number[],
501
+ mod:string,
502
+ caller:string,
503
+ referrer:string = ZERO_ADDRESS,
504
+ ) => {
505
+ const modFee = await market.read.getModFee([mod]);
506
+ const referrerFee = await totems.read.getFee([referrer]);
507
+ return await proxyMod.write.addMod([
508
+ ticker,
509
+ hooks,
510
+ mod,
511
+ referrer,
512
+ ], { account: caller, value: modFee + referrerFee });
513
+ }
514
+
515
+ export const removeMod = async (
516
+ proxyMod:any,
517
+ ticker:string,
518
+ mod:string,
519
+ caller:string,
520
+ ) => {
521
+ return await proxyMod.write.removeMod([
522
+ ticker,
523
+ mod,
524
+ ], { account: caller });
525
+ }