ff1-cli 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 +21 -0
- package/README.md +65 -0
- package/config.json.example +78 -0
- package/dist/index.js +627 -0
- package/dist/src/ai-orchestrator/index.js +870 -0
- package/dist/src/ai-orchestrator/registry.js +96 -0
- package/dist/src/config.js +352 -0
- package/dist/src/intent-parser/index.js +1342 -0
- package/dist/src/intent-parser/utils.js +108 -0
- package/dist/src/logger.js +72 -0
- package/dist/src/main.js +393 -0
- package/dist/src/types.js +5 -0
- package/dist/src/utilities/address-validator.js +242 -0
- package/dist/src/utilities/domain-resolver.js +291 -0
- package/dist/src/utilities/feed-fetcher.js +387 -0
- package/dist/src/utilities/ff1-device.js +176 -0
- package/dist/src/utilities/functions.js +325 -0
- package/dist/src/utilities/index.js +372 -0
- package/dist/src/utilities/nft-indexer.js +1013 -0
- package/dist/src/utilities/playlist-builder.js +522 -0
- package/dist/src/utilities/playlist-publisher.js +131 -0
- package/dist/src/utilities/playlist-send.js +241 -0
- package/dist/src/utilities/playlist-signer.js +171 -0
- package/dist/src/utilities/playlist-verifier.js +156 -0
- package/dist/src/utils.js +48 -0
- package/docs/CONFIGURATION.md +178 -0
- package/docs/EXAMPLES.md +331 -0
- package/docs/FUNCTION_CALLING.md +92 -0
- package/docs/README.md +267 -0
- package/docs/RELEASING.md +22 -0
- package/package.json +75 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Function Calling Layer
|
|
3
|
+
* These are the actual implementations called by AI orchestrator via function calling
|
|
4
|
+
*/
|
|
5
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
6
|
+
if (k2 === undefined) k2 = k;
|
|
7
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
8
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
9
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
10
|
+
}
|
|
11
|
+
Object.defineProperty(o, k2, desc);
|
|
12
|
+
}) : (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
o[k2] = m[k];
|
|
15
|
+
}));
|
|
16
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
17
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
18
|
+
}) : function(o, v) {
|
|
19
|
+
o["default"] = v;
|
|
20
|
+
});
|
|
21
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
22
|
+
var ownKeys = function(o) {
|
|
23
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
24
|
+
var ar = [];
|
|
25
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
26
|
+
return ar;
|
|
27
|
+
};
|
|
28
|
+
return ownKeys(o);
|
|
29
|
+
};
|
|
30
|
+
return function (mod) {
|
|
31
|
+
if (mod && mod.__esModule) return mod;
|
|
32
|
+
var result = {};
|
|
33
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
34
|
+
__setModuleDefault(result, mod);
|
|
35
|
+
return result;
|
|
36
|
+
};
|
|
37
|
+
})();
|
|
38
|
+
const chalk = require('chalk');
|
|
39
|
+
const playlistBuilder = require('./playlist-builder');
|
|
40
|
+
const ff1Device = require('./ff1-device');
|
|
41
|
+
const domainResolver = require('./domain-resolver');
|
|
42
|
+
/**
|
|
43
|
+
* Build DP1 v1.0.0 compliant playlist
|
|
44
|
+
*
|
|
45
|
+
* This is the actual implementation called by AI orchestrator's function calling.
|
|
46
|
+
* Uses core playlist-builder utilities.
|
|
47
|
+
*
|
|
48
|
+
* @param {Object} params - Build parameters
|
|
49
|
+
* @param {Array<Object>} params.items - Playlist items
|
|
50
|
+
* @param {string} [params.title] - Playlist title (auto-generated if not provided)
|
|
51
|
+
* @param {string} [params.slug] - Playlist slug (auto-generated if not provided)
|
|
52
|
+
* @returns {Promise<Object>} DP1 playlist
|
|
53
|
+
* @example
|
|
54
|
+
* const playlist = await buildDP1Playlist({ items, title: 'My Playlist' });
|
|
55
|
+
*/
|
|
56
|
+
async function buildDP1Playlist(params) {
|
|
57
|
+
const { items, title, slug } = params;
|
|
58
|
+
return await playlistBuilder.buildDP1Playlist({ items, title, slug });
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Send playlist to FF1 device
|
|
62
|
+
*
|
|
63
|
+
* This is the actual implementation called by AI orchestrator's function calling.
|
|
64
|
+
*
|
|
65
|
+
* @param {Object} params - Send parameters
|
|
66
|
+
* @param {Object} params.playlist - DP1 playlist
|
|
67
|
+
* @param {string} [params.deviceName] - Device name (null for first device)
|
|
68
|
+
* @returns {Promise<Object>} Result
|
|
69
|
+
* @returns {boolean} returns.success - Whether send succeeded
|
|
70
|
+
* @returns {string} [returns.deviceHost] - Device host address
|
|
71
|
+
* @returns {string} [returns.deviceName] - Device name
|
|
72
|
+
* @returns {string} [returns.error] - Error message if failed
|
|
73
|
+
* @example
|
|
74
|
+
* const result = await sendPlaylistToDevice({ playlist, deviceName: 'MyDevice' });
|
|
75
|
+
*/
|
|
76
|
+
async function sendPlaylistToDevice(params) {
|
|
77
|
+
const { playlist, deviceName } = params;
|
|
78
|
+
const result = await ff1Device.sendPlaylistToDevice({
|
|
79
|
+
playlist,
|
|
80
|
+
deviceName,
|
|
81
|
+
});
|
|
82
|
+
if (result.success) {
|
|
83
|
+
console.log(chalk.green('\n✓ Sent to device'));
|
|
84
|
+
if (result.deviceName) {
|
|
85
|
+
console.log(chalk.gray(` ${result.deviceName}`));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
console.error(chalk.red('\n✗ Could not send to device'));
|
|
90
|
+
if (result.error) {
|
|
91
|
+
console.error(chalk.red(` ${result.error}`));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Resolve blockchain domain names to addresses
|
|
98
|
+
*
|
|
99
|
+
* This is the actual implementation called by AI orchestrator's function calling.
|
|
100
|
+
* Supports ENS (.eth) and TNS (.tez) domains with batch resolution.
|
|
101
|
+
*
|
|
102
|
+
* @param {Object} params - Resolution parameters
|
|
103
|
+
* @param {Array<string>} params.domains - Array of domain names to resolve
|
|
104
|
+
* @param {boolean} [params.displayResults] - Whether to display results (default: true)
|
|
105
|
+
* @returns {Promise<Object>} Resolution result
|
|
106
|
+
* @returns {boolean} returns.success - Whether at least one domain was resolved
|
|
107
|
+
* @returns {Object} returns.domainMap - Map of domain to resolved address
|
|
108
|
+
* @returns {Array<Object>} returns.resolutions - Detailed resolution results
|
|
109
|
+
* @returns {Array<string>} returns.errors - Array of error messages
|
|
110
|
+
* @example
|
|
111
|
+
* const result = await resolveDomains({ domains: ['vitalik.eth', 'alice.tez'] });
|
|
112
|
+
* console.log(result.domainMap); // { 'vitalik.eth': '0x...', 'alice.tez': 'tz...' }
|
|
113
|
+
*/
|
|
114
|
+
async function resolveDomains(params) {
|
|
115
|
+
const { domains, displayResults = true } = params;
|
|
116
|
+
if (!domains || !Array.isArray(domains) || domains.length === 0) {
|
|
117
|
+
const error = 'No domains provided for resolution';
|
|
118
|
+
console.error(chalk.red(`\n✗ ${error}`));
|
|
119
|
+
return {
|
|
120
|
+
success: false,
|
|
121
|
+
domainMap: {},
|
|
122
|
+
resolutions: [],
|
|
123
|
+
errors: [error],
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
const result = await domainResolver.resolveDomainsBatch(domains);
|
|
127
|
+
if (displayResults) {
|
|
128
|
+
domainResolver.displayResolutionResults(result);
|
|
129
|
+
}
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Verify a playlist against DP-1 specification
|
|
134
|
+
*
|
|
135
|
+
* This is the actual implementation called by AI orchestrator's function calling.
|
|
136
|
+
* Uses dp1-js library for standards-compliant validation. Must be called before
|
|
137
|
+
* sending a playlist to a device.
|
|
138
|
+
*
|
|
139
|
+
* @param {Object} params - Verification parameters
|
|
140
|
+
* @param {Object} params.playlist - Playlist object to verify
|
|
141
|
+
* @returns {Promise<Object>} Verification result
|
|
142
|
+
* @returns {boolean} returns.valid - Whether playlist is valid
|
|
143
|
+
* @returns {string} [returns.error] - Error message if invalid
|
|
144
|
+
* @returns {Array<Object>} [returns.details] - Detailed validation errors
|
|
145
|
+
* @example
|
|
146
|
+
* const result = await verifyPlaylist({ playlist });
|
|
147
|
+
* if (result.valid) {
|
|
148
|
+
* console.log('Playlist is valid');
|
|
149
|
+
* } else {
|
|
150
|
+
* console.error('Invalid:', result.error);
|
|
151
|
+
* }
|
|
152
|
+
*/
|
|
153
|
+
async function verifyPlaylist(params) {
|
|
154
|
+
const { playlist } = params;
|
|
155
|
+
if (!playlist) {
|
|
156
|
+
return {
|
|
157
|
+
valid: false,
|
|
158
|
+
error: 'No playlist provided for verification',
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
console.log(chalk.cyan('\nValidating playlist...'));
|
|
162
|
+
// Dynamic import to avoid circular dependency
|
|
163
|
+
const playlistVerifier = await Promise.resolve().then(() => __importStar(require('./playlist-verifier')));
|
|
164
|
+
const verify = playlistVerifier.verifyPlaylist ||
|
|
165
|
+
(playlistVerifier.default && playlistVerifier.default.verifyPlaylist) ||
|
|
166
|
+
playlistVerifier.default;
|
|
167
|
+
if (typeof verify !== 'function') {
|
|
168
|
+
return {
|
|
169
|
+
valid: false,
|
|
170
|
+
error: 'Playlist verifier is not available',
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
const result = verify(playlist);
|
|
174
|
+
if (result.valid) {
|
|
175
|
+
console.log(chalk.green('✓ Playlist looks good'));
|
|
176
|
+
if (playlist.title) {
|
|
177
|
+
console.log(chalk.gray(` Title: ${playlist.title}`));
|
|
178
|
+
}
|
|
179
|
+
if (playlist.items) {
|
|
180
|
+
console.log(chalk.gray(` Items: ${playlist.items.length}`));
|
|
181
|
+
}
|
|
182
|
+
console.log();
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
console.error(chalk.red('✗ Playlist has issues'));
|
|
186
|
+
console.error(chalk.red(` ${result.error}`));
|
|
187
|
+
if (result.details && result.details.length > 0) {
|
|
188
|
+
console.log(chalk.yellow('\n Missing or invalid fields:'));
|
|
189
|
+
result.details.forEach((detail) => {
|
|
190
|
+
console.log(chalk.yellow(` • ${detail.path}: ${detail.message}`));
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
console.log();
|
|
194
|
+
}
|
|
195
|
+
return result;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Verify and validate Ethereum and Tezos addresses
|
|
199
|
+
*
|
|
200
|
+
* This function is called by the intent parser to validate addresses entered by users.
|
|
201
|
+
* It provides detailed feedback on address validity and format issues.
|
|
202
|
+
*
|
|
203
|
+
* @param {Object} params - Verification parameters
|
|
204
|
+
* @param {Array<string>} params.addresses - Array of addresses to verify
|
|
205
|
+
* @returns {Promise<Object>} Verification result
|
|
206
|
+
* @returns {boolean} returns.valid - Whether all addresses are valid
|
|
207
|
+
* @returns {Array<Object>} returns.results - Detailed validation for each address
|
|
208
|
+
* @returns {Array<string>} returns.errors - List of validation errors
|
|
209
|
+
* @example
|
|
210
|
+
* const result = await verifyAddresses({
|
|
211
|
+
* addresses: ['0x1234567890123456789012345678901234567890', 'tz1VSUr8wwNhLAzempoch5d6hLKEUNvD14']
|
|
212
|
+
* });
|
|
213
|
+
* if (!result.valid) {
|
|
214
|
+
* result.errors.forEach(err => console.error(err));
|
|
215
|
+
* }
|
|
216
|
+
*/
|
|
217
|
+
async function verifyAddresses(params) {
|
|
218
|
+
const { addresses } = params;
|
|
219
|
+
if (!addresses || !Array.isArray(addresses) || addresses.length === 0) {
|
|
220
|
+
return {
|
|
221
|
+
valid: false,
|
|
222
|
+
results: [],
|
|
223
|
+
errors: ['No addresses provided for verification'],
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
// Dynamic import to avoid circular dependency
|
|
227
|
+
const addressValidator = await Promise.resolve().then(() => __importStar(require('./address-validator')));
|
|
228
|
+
const validateAddresses = addressValidator.validateAddresses ||
|
|
229
|
+
(addressValidator.default && addressValidator.default.validateAddresses) ||
|
|
230
|
+
addressValidator.default;
|
|
231
|
+
if (typeof validateAddresses !== 'function') {
|
|
232
|
+
return {
|
|
233
|
+
valid: false,
|
|
234
|
+
results: [],
|
|
235
|
+
errors: ['Address validator is not available'],
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
const result = validateAddresses(addresses);
|
|
239
|
+
// Display results
|
|
240
|
+
if (result.valid) {
|
|
241
|
+
console.log(chalk.green('\n✓ All addresses are valid'));
|
|
242
|
+
result.results.forEach((r) => {
|
|
243
|
+
const typeLabel = r.type === 'ethereum'
|
|
244
|
+
? 'Ethereum'
|
|
245
|
+
: r.type === 'ens'
|
|
246
|
+
? 'ENS Domain'
|
|
247
|
+
: r.type === 'tezos-domain'
|
|
248
|
+
? 'Tezos Domain'
|
|
249
|
+
: r.type === 'contract'
|
|
250
|
+
? 'Tezos Contract'
|
|
251
|
+
: 'Tezos User';
|
|
252
|
+
console.log(chalk.gray(` • ${r.address} (${typeLabel})`));
|
|
253
|
+
if (r.normalized) {
|
|
254
|
+
console.log(chalk.gray(` Checksummed: ${r.normalized}`));
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
console.log();
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
console.error(chalk.red('\n✗ Address validation failed'));
|
|
261
|
+
result.errors.forEach((err) => {
|
|
262
|
+
console.error(chalk.red(` • ${err}`));
|
|
263
|
+
});
|
|
264
|
+
console.log();
|
|
265
|
+
}
|
|
266
|
+
return result;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Get list of configured FF1 devices
|
|
270
|
+
*
|
|
271
|
+
* This function retrieves the list of all configured FF1 devices from config.
|
|
272
|
+
* Called by intent parser to resolve generic device references like "FF1", "my device".
|
|
273
|
+
*
|
|
274
|
+
* @returns {Promise<Object>} Device list result
|
|
275
|
+
* @returns {boolean} returns.success - Whether devices were retrieved
|
|
276
|
+
* @returns {Array<Object>} returns.devices - Array of device configurations
|
|
277
|
+
* @returns {string} returns.devices[].name - Device name
|
|
278
|
+
* @returns {string} returns.devices[].host - Device host URL
|
|
279
|
+
* @returns {string} [returns.devices[].topicID] - Optional topic ID
|
|
280
|
+
* @returns {string} [returns.error] - Error message if no devices configured
|
|
281
|
+
* @example
|
|
282
|
+
* const result = await getConfiguredDevices();
|
|
283
|
+
* if (result.success && result.devices.length > 0) {
|
|
284
|
+
* const firstDevice = result.devices[0].name;
|
|
285
|
+
* }
|
|
286
|
+
*/
|
|
287
|
+
async function getConfiguredDevices() {
|
|
288
|
+
const configModule = await Promise.resolve().then(() => __importStar(require('../config')));
|
|
289
|
+
const getFF1DeviceConfig = configModule.getFF1DeviceConfig ||
|
|
290
|
+
(configModule.default && configModule.default.getFF1DeviceConfig) ||
|
|
291
|
+
configModule.default;
|
|
292
|
+
if (typeof getFF1DeviceConfig !== 'function') {
|
|
293
|
+
return {
|
|
294
|
+
success: false,
|
|
295
|
+
devices: [],
|
|
296
|
+
error: 'FF1 device configuration is not available',
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
const deviceConfig = getFF1DeviceConfig();
|
|
300
|
+
if (!deviceConfig.devices || deviceConfig.devices.length === 0) {
|
|
301
|
+
return {
|
|
302
|
+
success: false,
|
|
303
|
+
devices: [],
|
|
304
|
+
error: 'No FF1 devices configured',
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
// Return sanitized device list (without API keys)
|
|
308
|
+
const devices = deviceConfig.devices.map((d) => ({
|
|
309
|
+
name: d.name || d.host,
|
|
310
|
+
host: d.host,
|
|
311
|
+
topicID: d.topicID,
|
|
312
|
+
}));
|
|
313
|
+
return {
|
|
314
|
+
success: true,
|
|
315
|
+
devices,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
module.exports = {
|
|
319
|
+
buildDP1Playlist,
|
|
320
|
+
sendPlaylistToDevice,
|
|
321
|
+
resolveDomains,
|
|
322
|
+
verifyPlaylist,
|
|
323
|
+
getConfiguredDevices,
|
|
324
|
+
verifyAddresses,
|
|
325
|
+
};
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities - Actual function implementations
|
|
3
|
+
* Contains the real logic for querying NFTs and building playlists
|
|
4
|
+
*/
|
|
5
|
+
const chalk = require('chalk');
|
|
6
|
+
const nftIndexer = require('./nft-indexer');
|
|
7
|
+
const feedFetcher = require('./feed-fetcher');
|
|
8
|
+
const playlistBuilder = require('./playlist-builder');
|
|
9
|
+
const functions = require('./functions');
|
|
10
|
+
const domainResolver = require('./domain-resolver');
|
|
11
|
+
/**
|
|
12
|
+
* Initialize utilities with configuration
|
|
13
|
+
*
|
|
14
|
+
* The indexer now uses a hardcoded production endpoint, so no configuration is needed.
|
|
15
|
+
* This function is kept for backwards compatibility.
|
|
16
|
+
*
|
|
17
|
+
* @param {Object} _config - Unused config parameter
|
|
18
|
+
*/
|
|
19
|
+
function initializeUtilities(_config) {
|
|
20
|
+
nftIndexer.initializeIndexer();
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Query tokens from an owner address
|
|
24
|
+
*
|
|
25
|
+
* Fetches all tokens owned by an address, with optional random selection.
|
|
26
|
+
* If no tokens found, instructs user to add address via Feral File mobile app.
|
|
27
|
+
* Supports pagination to fetch all tokens when quantity is "all".
|
|
28
|
+
*
|
|
29
|
+
* @param {string} ownerAddress - Owner wallet address
|
|
30
|
+
* @param {number|string} [quantity] - Number of random tokens to select, or "all" to fetch all tokens
|
|
31
|
+
* @param {number} duration - Duration per item in seconds
|
|
32
|
+
* @returns {Promise<Array>} Array of DP1 playlist items
|
|
33
|
+
*/
|
|
34
|
+
async function queryTokensByAddress(ownerAddress, quantity, duration = 10) {
|
|
35
|
+
try {
|
|
36
|
+
const shouldFetchAll = quantity === 'all' || quantity === undefined || quantity === null;
|
|
37
|
+
const batchSize = 100; // Fetch 100 tokens per page
|
|
38
|
+
let allTokens = [];
|
|
39
|
+
let offset = 0;
|
|
40
|
+
let hasMore = true;
|
|
41
|
+
// Fetch tokens with pagination if "all" is requested
|
|
42
|
+
if (shouldFetchAll) {
|
|
43
|
+
console.log(chalk.cyan(` Fetching all tokens from ${ownerAddress}...`));
|
|
44
|
+
while (hasMore) {
|
|
45
|
+
const result = await nftIndexer.queryTokensByOwner(ownerAddress, batchSize, offset);
|
|
46
|
+
if (!result.success) {
|
|
47
|
+
if (offset === 0) {
|
|
48
|
+
// First page failed
|
|
49
|
+
console.log(chalk.yellow(` Could not fetch tokens for ${ownerAddress}`));
|
|
50
|
+
console.log(chalk.cyan(` → Add this address in the Feral File mobile app, then try again with the CLI.`));
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
// Subsequent pages failed - stop pagination but keep what we have
|
|
54
|
+
hasMore = false;
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
if (result.tokens.length === 0) {
|
|
58
|
+
// No more tokens
|
|
59
|
+
hasMore = false;
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
allTokens = allTokens.concat(result.tokens);
|
|
63
|
+
offset += batchSize;
|
|
64
|
+
console.log(chalk.gray(` → Fetched ${allTokens.length} tokens so far...`));
|
|
65
|
+
// If we got fewer tokens than the batch size, we've reached the end
|
|
66
|
+
if (result.tokens.length < batchSize) {
|
|
67
|
+
hasMore = false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (allTokens.length === 0 && offset === 0) {
|
|
71
|
+
console.log(chalk.yellow(` No tokens found for ${ownerAddress}`));
|
|
72
|
+
console.log(chalk.cyan(` → Add this address in the Feral File mobile app, then try again with the CLI.`));
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
// Fetch specific quantity with single request
|
|
78
|
+
const limit = Math.min(quantity, 100); // Cap at 100 per request
|
|
79
|
+
const result = await nftIndexer.queryTokensByOwner(ownerAddress, limit);
|
|
80
|
+
if (!result.success) {
|
|
81
|
+
console.log(chalk.yellow(` Could not fetch tokens for ${ownerAddress}`));
|
|
82
|
+
console.log(chalk.cyan(` → Add this address in the Feral File mobile app, then try again with the CLI.`));
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
if (result.tokens.length === 0) {
|
|
86
|
+
console.log(chalk.yellow(` No tokens found for ${ownerAddress}`));
|
|
87
|
+
console.log(chalk.cyan(` → Add this address in the Feral File mobile app, then try again with the CLI.`));
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
allTokens = result.tokens;
|
|
91
|
+
}
|
|
92
|
+
let selectedTokens = allTokens;
|
|
93
|
+
// Apply quantity limit with random selection (only if numeric quantity specified)
|
|
94
|
+
if (typeof quantity === 'number' && selectedTokens.length > quantity) {
|
|
95
|
+
selectedTokens = shuffleArray([...selectedTokens]).slice(0, quantity);
|
|
96
|
+
}
|
|
97
|
+
console.log(chalk.grey(`✓ Got ${selectedTokens.length} token(s)`));
|
|
98
|
+
// Convert tokens to DP1 items
|
|
99
|
+
const items = [];
|
|
100
|
+
let skippedCount = 0;
|
|
101
|
+
for (const token of selectedTokens) {
|
|
102
|
+
// Detect blockchain from contract address (support both camelCase and snake_case)
|
|
103
|
+
let chain = 'ethereum';
|
|
104
|
+
const contractAddr = token.contract_address || token.contractAddress || '';
|
|
105
|
+
if (contractAddr.startsWith('KT')) {
|
|
106
|
+
chain = 'tezos';
|
|
107
|
+
}
|
|
108
|
+
// Map indexer token data to standard format
|
|
109
|
+
const tokenData = nftIndexer.mapIndexerDataToStandardFormat(token, chain);
|
|
110
|
+
if (tokenData.success) {
|
|
111
|
+
const dp1Result = nftIndexer.convertToDP1Item(tokenData, duration);
|
|
112
|
+
if (dp1Result.success && dp1Result.item) {
|
|
113
|
+
items.push(dp1Result.item);
|
|
114
|
+
}
|
|
115
|
+
else if (!dp1Result.success) {
|
|
116
|
+
skippedCount++;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (skippedCount > 0) {
|
|
121
|
+
console.log(chalk.yellow(` ⚠ Skipped ${skippedCount} token(s) with invalid data (data URIs or URLs too long)`));
|
|
122
|
+
}
|
|
123
|
+
return items;
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
console.error(chalk.red(` Error: ${error.message}\n`));
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Query data for a single requirement (handles build_playlist, fetch_feed, and query_address)
|
|
132
|
+
*
|
|
133
|
+
* @param {Object} requirement - Requirement object
|
|
134
|
+
* @param {string} requirement.type - Requirement type (build_playlist, fetch_feed, or query_address)
|
|
135
|
+
* @param {string} [requirement.blockchain] - Blockchain network (for build_playlist)
|
|
136
|
+
* @param {string} [requirement.contractAddress] - Contract address (for build_playlist)
|
|
137
|
+
* @param {Array<string>} [requirement.tokenIds] - Token IDs (for build_playlist)
|
|
138
|
+
* @param {string} [requirement.ownerAddress] - Owner address (for query_address)
|
|
139
|
+
* @param {string} [requirement.playlistName] - Feed playlist name (for fetch_feed)
|
|
140
|
+
* @param {number} [requirement.quantity] - Number of items
|
|
141
|
+
* @param {number} duration - Duration per item in seconds
|
|
142
|
+
* @returns {Promise<Array>} Array of DP1 playlist items
|
|
143
|
+
*/
|
|
144
|
+
async function queryRequirement(requirement, duration = 10) {
|
|
145
|
+
const { type, blockchain, contractAddress, tokenIds, ownerAddress, playlistName, quantity } = requirement;
|
|
146
|
+
// Handle query_address type
|
|
147
|
+
if (type === 'query_address') {
|
|
148
|
+
// Check if ownerAddress is a domain name (.eth or .tez)
|
|
149
|
+
if (ownerAddress && (ownerAddress.endsWith('.eth') || ownerAddress.endsWith('.tez'))) {
|
|
150
|
+
console.log(chalk.cyan(`\nResolving domain ${ownerAddress}...`));
|
|
151
|
+
const resolution = await domainResolver.resolveDomain(ownerAddress);
|
|
152
|
+
if (resolution.resolved && resolution.address) {
|
|
153
|
+
console.log(chalk.gray(` ${resolution.domain} → ${resolution.address}`));
|
|
154
|
+
// Use resolved address instead of domain
|
|
155
|
+
return await queryTokensByAddress(resolution.address, quantity, duration);
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
console.log(chalk.red(` Could not resolve domain ${ownerAddress}: ${resolution.error || 'Unknown error'}`));
|
|
159
|
+
return [];
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
return await queryTokensByAddress(ownerAddress, quantity, duration);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// Handle fetch_feed type
|
|
167
|
+
if (type === 'fetch_feed') {
|
|
168
|
+
console.log(chalk.cyan(`Getting items from "${playlistName}"...`));
|
|
169
|
+
const result = await feedFetcher.fetchFeedPlaylistDirect(playlistName, quantity, duration);
|
|
170
|
+
if (result.success && result.items) {
|
|
171
|
+
return result.items;
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
console.log(chalk.yellow(` Could not fetch playlist: ${result.error || 'No items found'}\n`));
|
|
175
|
+
return [];
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// Handle build_playlist type (original NFT querying logic)
|
|
179
|
+
console.log(chalk.cyan(`Querying ${blockchain}${contractAddress ? ' (' + contractAddress.substring(0, 10) + '...)' : ''}...`));
|
|
180
|
+
let items = [];
|
|
181
|
+
try {
|
|
182
|
+
// Handle different blockchain types
|
|
183
|
+
if (blockchain.toLowerCase() === 'tezos') {
|
|
184
|
+
// Tezos NFTs
|
|
185
|
+
if (tokenIds && tokenIds.length > 0) {
|
|
186
|
+
const tokens = tokenIds.map((tokenId) => ({
|
|
187
|
+
chain: 'tezos',
|
|
188
|
+
contractAddress,
|
|
189
|
+
tokenId,
|
|
190
|
+
}));
|
|
191
|
+
items = await nftIndexer.getNFTTokenInfoBatch(tokens, duration);
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
console.log(chalk.yellow(' No token IDs specified'));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
else if (blockchain.toLowerCase() === 'ethereum' || blockchain.toLowerCase() === 'eth') {
|
|
198
|
+
// Ethereum NFTs (including Art Blocks, Feral File, etc.)
|
|
199
|
+
if (contractAddress && tokenIds && tokenIds.length > 0) {
|
|
200
|
+
const tokens = tokenIds.map((tokenId) => ({
|
|
201
|
+
chain: 'ethereum',
|
|
202
|
+
contractAddress,
|
|
203
|
+
tokenId,
|
|
204
|
+
}));
|
|
205
|
+
items = await nftIndexer.getNFTTokenInfoBatch(tokens, duration);
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
console.log(chalk.yellow(' Contract address and token IDs required'));
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
console.log(chalk.yellow(` Unsupported blockchain: ${blockchain}`));
|
|
213
|
+
}
|
|
214
|
+
if (items.length > 0) {
|
|
215
|
+
console.log(chalk.green(`✓ Got ${items.length} item(s)`));
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
console.log(chalk.yellow(` No items found. Check token IDs, contract address, or try querying by owner address.`));
|
|
219
|
+
}
|
|
220
|
+
// Apply quantity limit
|
|
221
|
+
if (quantity && items.length > quantity) {
|
|
222
|
+
items = items.slice(0, quantity);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
console.error(chalk.red(` Error: ${error.message}\n`));
|
|
227
|
+
throw error;
|
|
228
|
+
}
|
|
229
|
+
return items;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Build DP1 playlist from items
|
|
233
|
+
*
|
|
234
|
+
* Uses the core playlist-builder utility to create a DP1 v1.0.0 compliant playlist.
|
|
235
|
+
*
|
|
236
|
+
* @param {Array<Object>} items - Array of DP1 playlist items
|
|
237
|
+
* @param {string} [title] - Playlist title
|
|
238
|
+
* @param {string} [slug] - Playlist slug
|
|
239
|
+
* @returns {Promise<Object>} DP1 playlist
|
|
240
|
+
* @example
|
|
241
|
+
* const playlist = await buildDP1Playlist(items, 'My Playlist', 'my-playlist');
|
|
242
|
+
*/
|
|
243
|
+
async function buildDP1Playlist(items, title, slug) {
|
|
244
|
+
return await playlistBuilder.buildDP1Playlist({ items, title, slug });
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Send playlist to FF1 device
|
|
248
|
+
*
|
|
249
|
+
* @param {Object} playlist - DP1 playlist
|
|
250
|
+
* @param {string} [deviceName] - Device name
|
|
251
|
+
* @returns {Promise<Object>} Result
|
|
252
|
+
*/
|
|
253
|
+
async function sendToDevice(playlist, deviceName) {
|
|
254
|
+
const { sendPlaylistToDevice } = require('./functions');
|
|
255
|
+
return await sendPlaylistToDevice({ playlist, deviceName });
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Shuffle array using Fisher-Yates algorithm
|
|
259
|
+
*
|
|
260
|
+
* @param {Array} array - Array to shuffle
|
|
261
|
+
* @returns {Array} Shuffled array
|
|
262
|
+
*/
|
|
263
|
+
function shuffleArray(array) {
|
|
264
|
+
for (let i = array.length - 1; i > 0; i--) {
|
|
265
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
266
|
+
[array[i], array[j]] = [array[j], array[i]];
|
|
267
|
+
}
|
|
268
|
+
return array;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Build playlist directly from requirements (deterministic, no AI)
|
|
272
|
+
*
|
|
273
|
+
* @param {Object} params - Playlist parameters
|
|
274
|
+
* @param {Array<Object>} params.requirements - Array of requirements
|
|
275
|
+
* @param {Object} params.playlistSettings - Playlist settings
|
|
276
|
+
* @param {Object} options - Build options
|
|
277
|
+
* @param {boolean} [options.verbose] - Verbose output
|
|
278
|
+
* @param {string} [options.outputPath] - Output file path
|
|
279
|
+
* @returns {Promise<Object>} Result with playlist
|
|
280
|
+
*/
|
|
281
|
+
async function buildPlaylistDirect(params, options = {}) {
|
|
282
|
+
const { requirements, playlistSettings } = params;
|
|
283
|
+
const { verbose = false, outputPath = 'playlist.json' } = options;
|
|
284
|
+
const allItems = [];
|
|
285
|
+
const duration = playlistSettings.durationPerItem || 10;
|
|
286
|
+
console.log(chalk.cyan('\nBuilding playlist from your requirements...\n'));
|
|
287
|
+
// Process each requirement
|
|
288
|
+
for (let i = 0; i < requirements.length; i++) {
|
|
289
|
+
const requirement = requirements[i];
|
|
290
|
+
const reqNum = i + 1;
|
|
291
|
+
console.log(chalk.cyan(`[${reqNum}/${requirements.length}] ${requirement.blockchain || 'Source'}`));
|
|
292
|
+
try {
|
|
293
|
+
const items = await queryRequirement(requirement, duration);
|
|
294
|
+
allItems.push(...items);
|
|
295
|
+
}
|
|
296
|
+
catch (error) {
|
|
297
|
+
console.error(chalk.red(` ✗ Failed: ${error.message}`));
|
|
298
|
+
if (verbose) {
|
|
299
|
+
console.error(chalk.gray(error.stack));
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (allItems.length === 0) {
|
|
304
|
+
throw new Error('No items collected from any requirement');
|
|
305
|
+
}
|
|
306
|
+
// Apply ordering
|
|
307
|
+
let finalItems = allItems;
|
|
308
|
+
if (!playlistSettings.preserveOrder) {
|
|
309
|
+
console.log(chalk.cyan('Shuffling items...'));
|
|
310
|
+
finalItems = shuffleArray([...allItems]);
|
|
311
|
+
}
|
|
312
|
+
console.log(chalk.cyan(`Creating playlist with ${finalItems.length} items...`));
|
|
313
|
+
// Build DP1 playlist
|
|
314
|
+
const playlist = await buildDP1Playlist(finalItems, playlistSettings.title, playlistSettings.slug);
|
|
315
|
+
// Save playlist to file
|
|
316
|
+
const { savePlaylist } = require('../utils');
|
|
317
|
+
const savedPath = await savePlaylist(playlist, outputPath);
|
|
318
|
+
console.log(chalk.green(`✓ Playlist ready: ${savedPath}`));
|
|
319
|
+
// Send to device if requested
|
|
320
|
+
if (playlistSettings.deviceName !== undefined) {
|
|
321
|
+
console.log(chalk.cyan('\nSending to device...'));
|
|
322
|
+
await sendToDevice(playlist, playlistSettings.deviceName);
|
|
323
|
+
}
|
|
324
|
+
// Publish to feed server if requested
|
|
325
|
+
let publishResult = null;
|
|
326
|
+
if (playlistSettings.feedServer) {
|
|
327
|
+
console.log(chalk.cyan('\nPublishing to feed server...'));
|
|
328
|
+
try {
|
|
329
|
+
const { publishPlaylist } = require('./playlist-publisher');
|
|
330
|
+
publishResult = await publishPlaylist(savedPath, playlistSettings.feedServer.baseUrl, playlistSettings.feedServer.apiKey);
|
|
331
|
+
if (publishResult.success) {
|
|
332
|
+
console.log(chalk.green(`✓ Published to feed server`));
|
|
333
|
+
if (publishResult.playlistId) {
|
|
334
|
+
console.log(chalk.gray(` Playlist ID: ${publishResult.playlistId}`));
|
|
335
|
+
}
|
|
336
|
+
if (publishResult.feedServer) {
|
|
337
|
+
console.log(chalk.gray(` Server: ${publishResult.feedServer}`));
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
console.error(chalk.red(`✗ Failed to publish: ${publishResult.error}`));
|
|
342
|
+
if (publishResult.message) {
|
|
343
|
+
console.error(chalk.gray(` ${publishResult.message}`));
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
catch (error) {
|
|
348
|
+
console.error(chalk.red(`✗ Failed to publish: ${error.message}`));
|
|
349
|
+
if (verbose) {
|
|
350
|
+
console.error(chalk.gray(error.stack));
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return {
|
|
355
|
+
playlist,
|
|
356
|
+
published: publishResult?.success || false,
|
|
357
|
+
publishResult,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
module.exports = {
|
|
361
|
+
initializeUtilities,
|
|
362
|
+
queryRequirement,
|
|
363
|
+
queryTokensByAddress,
|
|
364
|
+
buildDP1Playlist,
|
|
365
|
+
sendToDevice,
|
|
366
|
+
resolveDomains: functions.resolveDomains,
|
|
367
|
+
shuffleArray,
|
|
368
|
+
buildPlaylistDirect,
|
|
369
|
+
feedFetcher,
|
|
370
|
+
// Export core playlist builder utilities
|
|
371
|
+
playlistBuilder,
|
|
372
|
+
};
|