Monorepo for Aesthetic.Computer
aesthetic.computer
1/**
2 * Keeps v4 Test Helper
3 *
4 * Shared utilities for testing the Keeps FA2 v4 contract.
5 * Provides Tezos client setup, credential management, and common helpers.
6 */
7
8import fs from 'fs';
9import path from 'path';
10import dotenv from 'dotenv';
11import { TezosToolkit } from '@taquito/taquito';
12import { InMemorySigner } from '@taquito/signer';
13import { fileURLToPath } from 'url';
14
15const __filename = fileURLToPath(import.meta.url);
16const __dirname = path.dirname(__filename);
17
18function envBool(value, fallback = false) {
19 if (value === undefined) return fallback;
20 return String(value).toLowerCase() === 'true';
21}
22
23function envNat(value, fallback) {
24 if (value === undefined || value === '') return fallback;
25 const parsed = Number.parseInt(String(value), 10);
26 return Number.isNaN(parsed) ? fallback : parsed;
27}
28
29const MAINNET_CONTRACT_DEFAULT = 'KT1QdGZP8jzqaxXDia3U7DYEqFYhfqGRHido'; // v5 RC
30const MAINNET_ADMIN_DEFAULT = 'tz1dfoQDuxjwSgxdqJnisyKUxDHweade4Gzt';
31const MAINNET_RPC_DEFAULT = 'https://mainnet.api.tez.ie';
32const GHOSTNET_RPC_DEFAULT = 'https://rpc.ghostnet.teztnets.com';
33
34// Contract addresses (override for v6 audits with env vars)
35export const CONTRACTS = {
36 mainnet:
37 process.env.KEEPS_AUDIT_CONTRACT ||
38 process.env.KEEPS_CONTRACT ||
39 process.env.TEZOS_KEEPS_CONTRACT ||
40 MAINNET_CONTRACT_DEFAULT,
41 mainnet_v4: 'KT1ER1GyoeRNhkv6E57yKbBbEKi5ynKbaH3W', // v4 staging (legacy)
42 ghostnet: process.env.KEEPS_GHOSTNET_CONTRACT || null // To be deployed for testing
43};
44
45// RPC endpoints (override per network if needed)
46export const RPCS = {
47 mainnet: process.env.KEEPS_MAINNET_RPC || MAINNET_RPC_DEFAULT,
48 ghostnet: process.env.KEEPS_GHOSTNET_RPC || GHOSTNET_RPC_DEFAULT
49};
50
51// Expected storage values (override for deployed v6 contract checks)
52export const EXPECTED_STORAGE = {
53 mainnet: {
54 administrator:
55 process.env.KEEPS_EXPECTED_ADMIN ||
56 process.env.KEEPS_ADMIN ||
57 MAINNET_ADMIN_DEFAULT,
58 default_royalty_bps: envNat(process.env.KEEPS_EXPECTED_ROYALTY_BPS, 1000), // 10%
59 paused: envBool(process.env.KEEPS_EXPECTED_PAUSED, false)
60 }
61};
62
63/**
64 * Load credentials from staging/.env file
65 * @param {string} wallet - Wallet name (default: 'staging')
66 * @returns {Promise<{address: string, secretKey: string}>}
67 */
68export async function loadCredentials(wallet = 'staging') {
69 const envPath = path.join(
70 __dirname,
71 '../../tezos/staging/.env'
72 );
73
74 if (!fs.existsSync(envPath)) {
75 throw new Error(`Credentials not found: ${envPath}`);
76 }
77
78 const envContent = fs.readFileSync(envPath, 'utf8');
79 const config = {};
80
81 // Parse .env file manually
82 envContent.split('\n').forEach(line => {
83 const trimmed = line.trim();
84 if (!trimmed || trimmed.startsWith('#')) return;
85
86 const [key, ...valueParts] = trimmed.split('=');
87 if (key && valueParts.length > 0) {
88 config[key.trim()] = valueParts.join('=').trim().replace(/^["']|["']$/g, '');
89 }
90 });
91
92 const address = config.STAGING_ADDRESS || config.ADDRESS;
93 const secretKey = config.STAGING_KEY || config.KEY || config.SECRET_KEY;
94
95 if (!address || !secretKey) {
96 throw new Error('Could not load address or secret key from credentials file');
97 }
98
99 return { address, secretKey };
100}
101
102/**
103 * Create Tezos client (read-only or with signer)
104 * @param {string} network - Network name ('mainnet' or 'ghostnet')
105 * @param {boolean} withSigner - Whether to add signer for write operations
106 * @returns {Promise<TezosToolkit>}
107 */
108export async function createTezosClient(network = 'mainnet', withSigner = false) {
109 const rpc = RPCS[network];
110 if (!rpc) {
111 throw new Error(`Unknown network: ${network}`);
112 }
113
114 const tezos = new TezosToolkit(rpc);
115
116 if (withSigner) {
117 const creds = await loadCredentials();
118 const signer = new InMemorySigner(creds.secretKey);
119 tezos.setProvider({ signer });
120 console.log(`✅ Tezos client created for ${network} with signer (${creds.address})`);
121 } else {
122 console.log(`✅ Tezos client created for ${network} (read-only)`);
123 }
124
125 return tezos;
126}
127
128/**
129 * Get contract instance
130 * @param {string} network - Network name ('mainnet' or 'ghostnet')
131 * @param {boolean} withSigner - Whether to add signer for write operations
132 * @returns {Promise<{tezos: TezosToolkit, contract: any, address: string}>}
133 */
134export async function getContract(network = 'mainnet', withSigner = false) {
135 const tezos = await createTezosClient(network, withSigner);
136 const address = CONTRACTS[network];
137
138 if (!address) {
139 throw new Error(`No contract address for network: ${network}`);
140 }
141
142 console.log(`📋 Loading contract at ${address}...`);
143 const contract = await tezos.contract.at(address);
144 console.log(`✅ Contract loaded successfully`);
145
146 return { tezos, contract, address };
147}
148
149/**
150 * Convert string to hex bytes (for contract parameters)
151 * @param {string} str - String to convert
152 * @returns {string} Hex string
153 */
154export function stringToBytes(str) {
155 return Buffer.from(str, 'utf8').toString('hex');
156}
157
158/**
159 * Convert hex bytes to string
160 * @param {string} bytes - Hex string
161 * @returns {string} UTF-8 string
162 */
163export function bytesToString(bytes) {
164 return Buffer.from(bytes, 'hex').toString('utf8');
165}
166
167/**
168 * Wait for operation confirmation
169 * @param {any} operation - Taquito operation
170 * @param {number} confirmations - Number of confirmations to wait for
171 * @returns {Promise<any>}
172 */
173export async function waitForConfirmation(operation, confirmations = 1) {
174 console.log(`⏳ Waiting for ${confirmations} confirmation(s)...`);
175 await operation.confirmation(confirmations);
176 console.log(`✅ Operation confirmed: ${operation.hash}`);
177 return operation;
178}
179
180/**
181 * Create valid keep parameters for testing
182 * @param {object} overrides - Override default parameters
183 * @returns {object} Keep parameters
184 */
185export function createKeepParams(overrides = {}) {
186 const defaults = {
187 name: stringToBytes("Test Keep"),
188 description: stringToBytes("Test description"),
189 artifactUri: stringToBytes("ipfs://QmTest123"),
190 displayUri: stringToBytes("ipfs://QmTest123"),
191 thumbnailUri: stringToBytes("ipfs://QmTestThumb"),
192 decimals: stringToBytes("0"),
193 symbol: stringToBytes("KEEP"),
194 isBooleanAmount: stringToBytes("true"),
195 shouldPreferSymbol: stringToBytes("false"),
196 formats: stringToBytes('[{"uri":"ipfs://QmTest123","mimeType":"text/html"}]'),
197 tags: stringToBytes('["test"]'),
198 attributes: stringToBytes('[]'),
199 creators: stringToBytes('[{"address":"tz1test","share":"100"}]'),
200 royalties: stringToBytes('[{"address":"tz1test","share":"100"}]'),
201 rights: stringToBytes("© 2025 Test"),
202 content_type: stringToBytes("text/html"),
203 content_hash: stringToBytes(`test-hash-${Date.now()}`),
204 metadata_uri: stringToBytes("ipfs://QmTestMeta"),
205 owner: "tz1dfoQDuxjwSgxdqJnisyKUxDHweade4Gzt"
206 };
207
208 return { ...defaults, ...overrides };
209}
210
211/**
212 * Sleep for specified milliseconds
213 * @param {number} ms - Milliseconds to sleep
214 * @returns {Promise<void>}
215 */
216export function sleep(ms) {
217 return new Promise(resolve => setTimeout(resolve, ms));
218}