@totems/evm 1.0.0
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/LICENSE +261 -0
- package/README.md +28 -0
- package/contracts/Errors.sol +29 -0
- package/contracts/ModMarket.sol +467 -0
- package/contracts/ReentrancyGuard.sol +17 -0
- package/contracts/Shared.sol +45 -0
- package/contracts/Totems.sol +835 -0
- package/interfaces/IMarket.sol +153 -0
- package/interfaces/IRelayFactory.sol +13 -0
- package/interfaces/ITotemTypes.sol +200 -0
- package/interfaces/ITotems.sol +178 -0
- package/mods/TotemMod.sol +136 -0
- package/mods/TotemsLibrary.sol +161 -0
- package/package.json +35 -0
- package/test/helpers.ts +466 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.28;
|
|
3
|
+
|
|
4
|
+
import "../interfaces/ITotems.sol";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @title TotemsLibrary
|
|
8
|
+
* @notice Helper library for mods to interact with the Totems contract
|
|
9
|
+
* @dev Provides convenient wrappers around ITotems functions and ticker validation utilities.
|
|
10
|
+
* All functions take the totems contract address as the first parameter, allowing mods
|
|
11
|
+
* to work with any Totems deployment.
|
|
12
|
+
*/
|
|
13
|
+
library TotemsLibrary {
|
|
14
|
+
|
|
15
|
+
/// @notice Thrown when a ticker contains an invalid character (not A-Z)
|
|
16
|
+
/// @param char The invalid ASCII character code
|
|
17
|
+
error InvalidTickerChar(uint8 char);
|
|
18
|
+
|
|
19
|
+
/// @notice Thrown when a ticker is empty or exceeds 10 characters
|
|
20
|
+
/// @param length The invalid ticker length
|
|
21
|
+
error InvalidTickerLength(uint256 length);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @notice Get the creator address of a totem
|
|
25
|
+
* @param totems The Totems contract address
|
|
26
|
+
* @param ticker The totem ticker symbol
|
|
27
|
+
* @return The address that created the totem
|
|
28
|
+
*/
|
|
29
|
+
function getCreator(address totems, string memory ticker) internal view returns (address) {
|
|
30
|
+
return ITotems(totems).getTotem(ticker).creator;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @notice Require that the calling contract (mod) is licensed for a totem
|
|
35
|
+
* @dev Reverts with "Totem not licensed" if the mod is not licensed
|
|
36
|
+
* @param totems The Totems contract address
|
|
37
|
+
* @param ticker The totem ticker symbol
|
|
38
|
+
*/
|
|
39
|
+
function checkLicense(address totems, string memory ticker) internal view {
|
|
40
|
+
require(hasLicense(totems, ticker, address(this)) == true, "Totem not licensed");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @notice Check if a mod is licensed to operate on a totem
|
|
45
|
+
* @param totems The Totems contract address
|
|
46
|
+
* @param ticker The totem ticker symbol
|
|
47
|
+
* @param mod The mod contract address to check
|
|
48
|
+
* @return True if the mod is licensed, false otherwise
|
|
49
|
+
*/
|
|
50
|
+
function hasLicense(
|
|
51
|
+
address totems,
|
|
52
|
+
string memory ticker,
|
|
53
|
+
address mod
|
|
54
|
+
) internal view returns (bool) {
|
|
55
|
+
return ITotems(totems).isLicensed(ticker, mod);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @notice Transfer tokens from this contract to another address
|
|
60
|
+
* @dev The calling contract must hold the tokens and be authorized to transfer
|
|
61
|
+
* @param totems The Totems contract address
|
|
62
|
+
* @param ticker The totem ticker symbol
|
|
63
|
+
* @param to The recipient address
|
|
64
|
+
* @param amount The amount of tokens to transfer
|
|
65
|
+
* @param memo Optional memo string for the transfer
|
|
66
|
+
*/
|
|
67
|
+
function transfer(
|
|
68
|
+
address totems,
|
|
69
|
+
string memory ticker,
|
|
70
|
+
address to,
|
|
71
|
+
uint256 amount,
|
|
72
|
+
string memory memo
|
|
73
|
+
) internal {
|
|
74
|
+
ITotems(totems).transfer(
|
|
75
|
+
ticker,
|
|
76
|
+
address(this),
|
|
77
|
+
to,
|
|
78
|
+
amount,
|
|
79
|
+
memo
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @notice Get the token balance of an account
|
|
85
|
+
* @param totems The Totems contract address
|
|
86
|
+
* @param ticker The totem ticker symbol
|
|
87
|
+
* @param account The address to check
|
|
88
|
+
* @return The token balance
|
|
89
|
+
*/
|
|
90
|
+
function getBalance(
|
|
91
|
+
address totems,
|
|
92
|
+
string memory ticker,
|
|
93
|
+
address account
|
|
94
|
+
) internal view returns (uint256) {
|
|
95
|
+
return ITotems(totems).getBalance(ticker, account);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* @notice Get the full totem data structure
|
|
100
|
+
* @param totems The Totems contract address
|
|
101
|
+
* @param ticker The totem ticker symbol
|
|
102
|
+
* @return The Totem struct containing all totem metadata
|
|
103
|
+
*/
|
|
104
|
+
function getTotem(
|
|
105
|
+
address totems,
|
|
106
|
+
string memory ticker
|
|
107
|
+
) internal view returns (ITotemTypes.Totem memory) {
|
|
108
|
+
return ITotems(totems).getTotem(ticker);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* @notice Get the statistics for a totem
|
|
113
|
+
* @param totems The Totems contract address
|
|
114
|
+
* @param ticker The totem ticker symbol
|
|
115
|
+
* @return The TotemStats struct containing supply, holder count, etc.
|
|
116
|
+
*/
|
|
117
|
+
function getTotemStats(
|
|
118
|
+
address totems,
|
|
119
|
+
string memory ticker
|
|
120
|
+
) internal view returns (ITotemTypes.TotemStats memory) {
|
|
121
|
+
return ITotems(totems).getStats(ticker);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* @notice Convert a ticker string to a deterministic bytes32 hash
|
|
126
|
+
* @dev Normalizes ticker to uppercase and validates characters.
|
|
127
|
+
* Only A-Z characters are allowed, max 10 characters.
|
|
128
|
+
* This ensures "btc", "BTC", and "Btc" all produce the same hash.
|
|
129
|
+
* @param ticker The ticker string (case-insensitive, 1-10 chars, A-Z only)
|
|
130
|
+
* @return The keccak256 hash of the normalized uppercase ticker
|
|
131
|
+
*/
|
|
132
|
+
function tickerToBytes(string calldata ticker) internal pure returns (bytes32) {
|
|
133
|
+
bytes calldata b = bytes(ticker);
|
|
134
|
+
uint256 len = b.length;
|
|
135
|
+
|
|
136
|
+
if (len == 0 || len > 10) {
|
|
137
|
+
revert InvalidTickerLength(len);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
bytes memory out = new bytes(len);
|
|
141
|
+
|
|
142
|
+
for (uint256 i = 0; i < len; i++) {
|
|
143
|
+
uint8 c = uint8(b[i]);
|
|
144
|
+
|
|
145
|
+
// Convert lowercase a-z (0x61-0x7A) to uppercase A-Z (0x41-0x5A)
|
|
146
|
+
if (c >= 0x61 && c <= 0x7A) {
|
|
147
|
+
c -= 32;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Reject any character outside A-Z range
|
|
151
|
+
if (c < 0x41 || c > 0x5A) {
|
|
152
|
+
revert InvalidTickerChar(c);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
out[i] = bytes1(c);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return keccak256(out);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@totems/evm",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Totems EVM smart contracts for building modular token systems",
|
|
5
|
+
"author": "nsjames",
|
|
6
|
+
"license": "MIT & AGPL-3.0",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/nsjames/totems-evm",
|
|
10
|
+
"directory": "package"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"solidity",
|
|
14
|
+
"ethereum",
|
|
15
|
+
"base",
|
|
16
|
+
"bsc",
|
|
17
|
+
"evm",
|
|
18
|
+
"tokens",
|
|
19
|
+
"smart-contracts",
|
|
20
|
+
"totems"
|
|
21
|
+
],
|
|
22
|
+
"files": [
|
|
23
|
+
"contracts/**/*.sol",
|
|
24
|
+
"constants/**/*.sol",
|
|
25
|
+
"interfaces/**/*.sol",
|
|
26
|
+
"mods/**/*.sol",
|
|
27
|
+
"test/**/*",
|
|
28
|
+
"LICENSE",
|
|
29
|
+
"package.json",
|
|
30
|
+
"README.md"
|
|
31
|
+
],
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"@openzeppelin/contracts": "^5.0.0"
|
|
34
|
+
}
|
|
35
|
+
}
|
package/test/helpers.ts
ADDED
|
@@ -0,0 +1,466 @@
|
|
|
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
|
+
export const MIN_BASE_FEE = 500000000000000n; // 0.0005 ether
|
|
144
|
+
export const BURNED_FEE = 100000000000000n; // 0.0001 ether
|
|
145
|
+
|
|
146
|
+
export enum Hook {
|
|
147
|
+
Created = 0,
|
|
148
|
+
Mint = 1,
|
|
149
|
+
Burn = 2,
|
|
150
|
+
Transfer = 3,
|
|
151
|
+
TransferOwnership = 4,
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export const setupTotemsTest = async (minBaseFee: bigint = MIN_BASE_FEE, burnedFee: bigint = BURNED_FEE) => {
|
|
155
|
+
const { viem } = await network.connect();
|
|
156
|
+
const publicClient = await viem.getPublicClient();
|
|
157
|
+
// @ts-ignore
|
|
158
|
+
const walletClient = await viem.getWalletClient();
|
|
159
|
+
|
|
160
|
+
const addresses = await walletClient.getAddresses();
|
|
161
|
+
const proxyModInitializer = addresses[0];
|
|
162
|
+
const proxyMod = await viem.deployContract("ProxyMod", [
|
|
163
|
+
proxyModInitializer
|
|
164
|
+
]);
|
|
165
|
+
|
|
166
|
+
let market = await viem.deployContract("ModMarket", [minBaseFee, burnedFee]);
|
|
167
|
+
let totems:any = await viem.deployContract("Totems", [
|
|
168
|
+
market.address,
|
|
169
|
+
proxyMod.address,
|
|
170
|
+
minBaseFee,
|
|
171
|
+
burnedFee,
|
|
172
|
+
]);
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
// using these to validate the interfaces
|
|
176
|
+
totems = await viem.getContractAt("ITotems", totems.address);
|
|
177
|
+
// @ts-ignore
|
|
178
|
+
market = await viem.getContractAt("IMarket", market.address);
|
|
179
|
+
// initialize proxy mod
|
|
180
|
+
await proxyMod.write.initialize([totems.address, market.address], { account: proxyModInitializer });
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
viem,
|
|
184
|
+
publicClient,
|
|
185
|
+
market,
|
|
186
|
+
totems,
|
|
187
|
+
accounts: addresses.slice(0, addresses.length),
|
|
188
|
+
proxyModSeller: addresses[0],
|
|
189
|
+
proxyMod,
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
export const modDetails = (details?:any) => Object.assign({
|
|
196
|
+
name: "Test Mod",
|
|
197
|
+
summary: "A test mod",
|
|
198
|
+
markdown: "## Test Mod\nThis is a test mod.",
|
|
199
|
+
image: "https://example.com/image.png",
|
|
200
|
+
website: "https://example.com",
|
|
201
|
+
websiteTickerPath: "/path/to/{ticker}",
|
|
202
|
+
isMinter: false,
|
|
203
|
+
needsUnlimited: false,
|
|
204
|
+
}, details || {});
|
|
205
|
+
|
|
206
|
+
export const publishMod = async (
|
|
207
|
+
market:any,
|
|
208
|
+
seller:string,
|
|
209
|
+
contract:string,
|
|
210
|
+
hooks:number[] = [],
|
|
211
|
+
details = modDetails(),
|
|
212
|
+
requiredActions:any[] = [],
|
|
213
|
+
referrer = ZERO_ADDRESS,
|
|
214
|
+
price = 1_000_000n,
|
|
215
|
+
fee = undefined
|
|
216
|
+
) => {
|
|
217
|
+
fee = fee ?? await market.read.getFee([referrer]);
|
|
218
|
+
|
|
219
|
+
return market.write.publish([
|
|
220
|
+
contract,
|
|
221
|
+
hooks,
|
|
222
|
+
price,
|
|
223
|
+
details,
|
|
224
|
+
requiredActions,
|
|
225
|
+
referrer,
|
|
226
|
+
], { value: fee, account: seller });
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export const totemDetails = (ticker:string, decimals:number) => {
|
|
230
|
+
return {
|
|
231
|
+
ticker: ticker,
|
|
232
|
+
decimals: decimals,
|
|
233
|
+
name: `${ticker} Totem`,
|
|
234
|
+
description: `This is the ${ticker} totem.`,
|
|
235
|
+
image: `https://example.com/${ticker.toLowerCase()}.png`,
|
|
236
|
+
website: `https://example.com/${ticker.toLowerCase()}`,
|
|
237
|
+
seed: '0x1110762033e7a10db4502359a19a61eb81312834769b8419047a2c9ae03ee847',
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export const createTotem = async (
|
|
242
|
+
totems:any,
|
|
243
|
+
market:any,
|
|
244
|
+
creator:string,
|
|
245
|
+
ticker:string,
|
|
246
|
+
decimals:number,
|
|
247
|
+
allocations:any[],
|
|
248
|
+
mods?:{
|
|
249
|
+
transfer?:string[],
|
|
250
|
+
mint?:string[],
|
|
251
|
+
burn?:string[],
|
|
252
|
+
created?:string[],
|
|
253
|
+
transferOwnership?:string[]
|
|
254
|
+
},
|
|
255
|
+
referrer:string = ZERO_ADDRESS,
|
|
256
|
+
details:any = undefined,
|
|
257
|
+
) => {
|
|
258
|
+
const baseFee = await totems.read.getFee([referrer]);
|
|
259
|
+
|
|
260
|
+
const _mods = Object.assign({
|
|
261
|
+
transfer: [],
|
|
262
|
+
mint: [],
|
|
263
|
+
burn: [],
|
|
264
|
+
created: [],
|
|
265
|
+
transferOwnership: [],
|
|
266
|
+
}, mods || {});
|
|
267
|
+
const uniqueMods = new Set<string>();
|
|
268
|
+
Object.values(_mods).forEach((modList:any[]) => {
|
|
269
|
+
modList.forEach(m => uniqueMods.add(m));
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
const modsFee = await market.read.getModsFee([[...uniqueMods]]);
|
|
273
|
+
return await totems.write.create([
|
|
274
|
+
details ? Object.assign({
|
|
275
|
+
ticker,
|
|
276
|
+
decimals,
|
|
277
|
+
}, details) : totemDetails(ticker, decimals),
|
|
278
|
+
allocations.map(a => ({
|
|
279
|
+
...a,
|
|
280
|
+
label: a.label || "",
|
|
281
|
+
isMinter: a.hasOwnProperty('isMinter') ? a.isMinter : false,
|
|
282
|
+
})),
|
|
283
|
+
_mods,
|
|
284
|
+
referrer,
|
|
285
|
+
], { account: creator, value: baseFee + modsFee });
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export const transfer = async (
|
|
289
|
+
totems:any,
|
|
290
|
+
ticker:string,
|
|
291
|
+
from:string,
|
|
292
|
+
to:string,
|
|
293
|
+
amount:number|bigint,
|
|
294
|
+
memo:string = "",
|
|
295
|
+
) => {
|
|
296
|
+
return await totems.write.transfer([
|
|
297
|
+
ticker,
|
|
298
|
+
from,
|
|
299
|
+
to,
|
|
300
|
+
amount,
|
|
301
|
+
memo,
|
|
302
|
+
], { account: from });
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export const mint = async (
|
|
306
|
+
totems:any,
|
|
307
|
+
mod:string,
|
|
308
|
+
minter:string,
|
|
309
|
+
ticker:string,
|
|
310
|
+
amount:number|bigint,
|
|
311
|
+
memo:string = "",
|
|
312
|
+
payment:number|bigint = 0n,
|
|
313
|
+
) => {
|
|
314
|
+
return await totems.write.mint([
|
|
315
|
+
mod,
|
|
316
|
+
minter,
|
|
317
|
+
ticker,
|
|
318
|
+
amount,
|
|
319
|
+
memo,
|
|
320
|
+
], { account: minter, value: payment });
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export const burn = async (
|
|
324
|
+
totems:any,
|
|
325
|
+
ticker:string,
|
|
326
|
+
owner:string,
|
|
327
|
+
amount:number|bigint,
|
|
328
|
+
memo:string = "",
|
|
329
|
+
) => {
|
|
330
|
+
return await totems.write.burn([
|
|
331
|
+
ticker,
|
|
332
|
+
owner,
|
|
333
|
+
amount,
|
|
334
|
+
memo,
|
|
335
|
+
], { account: owner });
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export const getBalance = async (
|
|
339
|
+
totems:any,
|
|
340
|
+
ticker:string,
|
|
341
|
+
account:string,
|
|
342
|
+
) => {
|
|
343
|
+
return await totems.read.getBalance([ticker, account]);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export const getTotem = async (
|
|
347
|
+
totems:any,
|
|
348
|
+
ticker:string,
|
|
349
|
+
) => {
|
|
350
|
+
return await totems.read.getTotem([ticker]);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export const getTotems = async (
|
|
354
|
+
totems:any,
|
|
355
|
+
tickers:string[],
|
|
356
|
+
) => {
|
|
357
|
+
return await totems.read.getTotems([tickers]);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export const getStats = async (
|
|
361
|
+
totems:any,
|
|
362
|
+
ticker:string,
|
|
363
|
+
) => {
|
|
364
|
+
return await totems.read.getStats([ticker]);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export const transferOwnership = async (
|
|
368
|
+
totems:any,
|
|
369
|
+
ticker:string,
|
|
370
|
+
currentOwner:string,
|
|
371
|
+
newOwner:string,
|
|
372
|
+
) => {
|
|
373
|
+
return await totems.write.transferOwnership([
|
|
374
|
+
ticker,
|
|
375
|
+
newOwner,
|
|
376
|
+
], { account: currentOwner });
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export const getMod = async (
|
|
380
|
+
market:any,
|
|
381
|
+
mod:string,
|
|
382
|
+
) => {
|
|
383
|
+
return await market.read.getMod([mod]);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export const getMods = async (
|
|
387
|
+
market:any,
|
|
388
|
+
mods:string[],
|
|
389
|
+
) => {
|
|
390
|
+
return await market.read.getMods([mods]);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export const getModFee = async (
|
|
394
|
+
market:any,
|
|
395
|
+
mod:string,
|
|
396
|
+
) => {
|
|
397
|
+
return await market.read.getModFee([mod]);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export const getModsFee = async (
|
|
401
|
+
market:any,
|
|
402
|
+
mods:string[],
|
|
403
|
+
) => {
|
|
404
|
+
return await market.read.getModsFee([mods]);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export const isLicensed = async (
|
|
408
|
+
totems:any,
|
|
409
|
+
ticker:string,
|
|
410
|
+
mod:string,
|
|
411
|
+
) => {
|
|
412
|
+
return await totems.read.isLicensed([ticker, mod]);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export const getRelays = async (
|
|
416
|
+
totems:any,
|
|
417
|
+
ticker:string,
|
|
418
|
+
) => {
|
|
419
|
+
return await totems.read.getRelays([ticker]);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
export const getSupportedHooks = async (
|
|
423
|
+
market:any,
|
|
424
|
+
mod:string,
|
|
425
|
+
) => {
|
|
426
|
+
return await market.read.getSupportedHooks([mod]);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
export const isUnlimitedMinter = async (
|
|
430
|
+
market:any,
|
|
431
|
+
mod:string,
|
|
432
|
+
) => {
|
|
433
|
+
return await market.read.isUnlimitedMinter([mod]);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
export const addMod = async (
|
|
437
|
+
proxyMod:any,
|
|
438
|
+
totems:any,
|
|
439
|
+
market:any,
|
|
440
|
+
ticker:string,
|
|
441
|
+
hooks:number[],
|
|
442
|
+
mod:string,
|
|
443
|
+
caller:string,
|
|
444
|
+
referrer:string = ZERO_ADDRESS,
|
|
445
|
+
) => {
|
|
446
|
+
const modFee = await market.read.getModFee([mod]);
|
|
447
|
+
const referrerFee = await totems.read.getFee([referrer]);
|
|
448
|
+
return await proxyMod.write.addMod([
|
|
449
|
+
ticker,
|
|
450
|
+
hooks,
|
|
451
|
+
mod,
|
|
452
|
+
referrer,
|
|
453
|
+
], { account: caller, value: modFee + referrerFee });
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
export const removeMod = async (
|
|
457
|
+
proxyMod:any,
|
|
458
|
+
ticker:string,
|
|
459
|
+
mod:string,
|
|
460
|
+
caller:string,
|
|
461
|
+
) => {
|
|
462
|
+
return await proxyMod.write.removeMod([
|
|
463
|
+
ticker,
|
|
464
|
+
mod,
|
|
465
|
+
], { account: caller });
|
|
466
|
+
}
|