@explorins/pers-sdk-react-native 2.2.1 → 2.3.0-alpha.1
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/dist/index.js
CHANGED
|
@@ -2624,6 +2624,8 @@ exports.ApiKeyType = void 0;
|
|
|
2624
2624
|
ApiKeyType["BLOCKCHAIN_READER_JWT"] = "BLOCKCHAIN_READER_JWT";
|
|
2625
2625
|
ApiKeyType["TRANSACTION_JWT_ACCESS_TOKEN"] = "TRANSACTION_JWT_ACCESS_TOKEN";
|
|
2626
2626
|
ApiKeyType["WEBHOOK_OUTBOUND_JWT"] = "WEBHOOK_OUTBOUND_JWT";
|
|
2627
|
+
/** One-Time Token for passwordless auth flows (magic link, SMS, etc.) */
|
|
2628
|
+
ApiKeyType["ONE_TIME_TOKEN"] = "ONE_TIME_TOKEN";
|
|
2627
2629
|
// TODO: this needs to be removed. However, there is still dependency
|
|
2628
2630
|
ApiKeyType["TENANT_LEGACY_ADMIN"] = "TENANT_ADMIN_JWT";
|
|
2629
2631
|
// TENANT_SYSTEM_API_SECRET = 'TENANT_SYSTEM_API_SECRET',
|
|
@@ -3152,6 +3154,8 @@ exports.WebhookSource = void 0;
|
|
|
3152
3154
|
WebhookSource["USER"] = "USER";
|
|
3153
3155
|
WebhookSource["BUSINESS"] = "BUSINESS";
|
|
3154
3156
|
WebhookSource["TENANT"] = "TENANT";
|
|
3157
|
+
// System-level internal invocation (e.g., token metadata webhook fetch)
|
|
3158
|
+
WebhookSource["INTERNAL"] = "INTERNAL";
|
|
3155
3159
|
// External verified sources
|
|
3156
3160
|
WebhookSource["STRIPE"] = "STRIPE";
|
|
3157
3161
|
// Allow any external traffic (no verification)
|
|
@@ -3182,6 +3186,629 @@ exports.WebhookCallbackStatus = void 0;
|
|
|
3182
3186
|
WebhookCallbackStatus["CANCELLED"] = "CANCELLED";
|
|
3183
3187
|
})(exports.WebhookCallbackStatus || (exports.WebhookCallbackStatus = {}));
|
|
3184
3188
|
|
|
3189
|
+
/**
|
|
3190
|
+
* One-Time Token Authentication Methods
|
|
3191
|
+
*
|
|
3192
|
+
* Defines the passwordless authentication methods supported by OTT flow.
|
|
3193
|
+
* Used in magic link, SMS verification, and email code authentication.
|
|
3194
|
+
*/
|
|
3195
|
+
const OttAuthMethod = {
|
|
3196
|
+
/** Magic link sent via email */
|
|
3197
|
+
MAGIC_LINK: 'magic-link',
|
|
3198
|
+
/** Verification code sent via SMS */
|
|
3199
|
+
SMS: 'sms',
|
|
3200
|
+
/** Verification code sent via email */
|
|
3201
|
+
EMAIL_CODE: 'email-code',
|
|
3202
|
+
};
|
|
3203
|
+
|
|
3204
|
+
/**
|
|
3205
|
+
* Base array of all string-valued user model property keys.
|
|
3206
|
+
* Single source of truth for user field names across the codebase.
|
|
3207
|
+
*/
|
|
3208
|
+
const userModelFieldsArray = [
|
|
3209
|
+
'id',
|
|
3210
|
+
'email',
|
|
3211
|
+
'identifierEmail',
|
|
3212
|
+
'firstName',
|
|
3213
|
+
'lastName',
|
|
3214
|
+
'externalId',
|
|
3215
|
+
'instagramAccountId',
|
|
3216
|
+
'googleAccountName',
|
|
3217
|
+
];
|
|
3218
|
+
/** Iterable array of all user model fields */
|
|
3219
|
+
const userModelFields = [...userModelFieldsArray];
|
|
3220
|
+
/**
|
|
3221
|
+
* Fields that can be used to uniquely identify/lookup a user.
|
|
3222
|
+
* Used for wildcard search, user lookup queries.
|
|
3223
|
+
*/
|
|
3224
|
+
const uniqueUserIdentifierKeysArray = [
|
|
3225
|
+
'id',
|
|
3226
|
+
'email',
|
|
3227
|
+
'identifierEmail',
|
|
3228
|
+
'instagramAccountId',
|
|
3229
|
+
'externalId',
|
|
3230
|
+
];
|
|
3231
|
+
// Export the array
|
|
3232
|
+
const uniqueUserIdentifierKeys = [...uniqueUserIdentifierKeysArray];
|
|
3233
|
+
// Define identifier type configurations
|
|
3234
|
+
const identifierTypes = {
|
|
3235
|
+
id: {
|
|
3236
|
+
key: 'id',
|
|
3237
|
+
detect: (value) => /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value), // UUID pattern
|
|
3238
|
+
process: (value) => value
|
|
3239
|
+
},
|
|
3240
|
+
email: {
|
|
3241
|
+
key: 'email',
|
|
3242
|
+
detect: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
|
|
3243
|
+
process: (value) => value
|
|
3244
|
+
},
|
|
3245
|
+
instagramAccountId: {
|
|
3246
|
+
key: 'instagramAccountId',
|
|
3247
|
+
detect: (value) => /^@/.test(value) || value.toLowerCase().includes('instagram'),
|
|
3248
|
+
process: (value) => value.replace(/^@/, '') // Remove @ if present for consistency
|
|
3249
|
+
},
|
|
3250
|
+
externalId: {
|
|
3251
|
+
key: 'externalId',
|
|
3252
|
+
detect: () => true, // Default fallback
|
|
3253
|
+
process: (value) => value
|
|
3254
|
+
}
|
|
3255
|
+
};
|
|
3256
|
+
/**
|
|
3257
|
+
* Register a new identifier type configuration
|
|
3258
|
+
* @param key The identifier key
|
|
3259
|
+
* @param config The identifier configuration (without the key)
|
|
3260
|
+
*/
|
|
3261
|
+
const registerIdentifierType = (key, config) => {
|
|
3262
|
+
if (!uniqueUserIdentifierKeysArray.includes(key)) {
|
|
3263
|
+
console.warn(`Warning: Registering identifier type '${key}' that is not in uniqueUserIdentifierKeys`);
|
|
3264
|
+
}
|
|
3265
|
+
identifierTypes[key] = {
|
|
3266
|
+
key,
|
|
3267
|
+
...config
|
|
3268
|
+
};
|
|
3269
|
+
};
|
|
3270
|
+
/**
|
|
3271
|
+
* Detects the type of identifier from a wildcard input
|
|
3272
|
+
* @param wildcardValue The value to analyze
|
|
3273
|
+
* @returns Object containing the type and processed value
|
|
3274
|
+
*/
|
|
3275
|
+
const detectIdentifierType = (wildcardValue) => {
|
|
3276
|
+
if (wildcardValue === null || wildcardValue === undefined) {
|
|
3277
|
+
return { type: 'externalId', value: '' };
|
|
3278
|
+
}
|
|
3279
|
+
const value = String(wildcardValue);
|
|
3280
|
+
// Try each identifier type in priority order (id first to catch UUIDs)
|
|
3281
|
+
for (const type of ['id', 'email', 'instagramAccountId', 'externalId']) {
|
|
3282
|
+
const config = identifierTypes[type];
|
|
3283
|
+
if (config.detect(value)) {
|
|
3284
|
+
return { type, value: config.process(value) };
|
|
3285
|
+
}
|
|
3286
|
+
}
|
|
3287
|
+
// Default fallback should never be reached due to externalId's detect always returning true
|
|
3288
|
+
return { type: 'externalId', value };
|
|
3289
|
+
};
|
|
3290
|
+
/**
|
|
3291
|
+
* Checks if an object is a properly formed UserIdentifierObject
|
|
3292
|
+
* @param obj Object to check
|
|
3293
|
+
* @returns Boolean indicating if the object is a UserIdentifierObject
|
|
3294
|
+
*/
|
|
3295
|
+
const isUserIdentifierObject = (obj) => {
|
|
3296
|
+
return obj &&
|
|
3297
|
+
obj.type === 'identifierObject' &&
|
|
3298
|
+
uniqueUserIdentifierKeys.every(key => key in obj);
|
|
3299
|
+
};
|
|
3300
|
+
/**
|
|
3301
|
+
* Creates a UserIdentifierObject with all required fields, setting missing values to null
|
|
3302
|
+
* @param params Partial parameters for the identifier object
|
|
3303
|
+
* @returns A properly formed UserIdentifierObject
|
|
3304
|
+
*/
|
|
3305
|
+
const createUserIdentifierObject = (params) => {
|
|
3306
|
+
// Extract the wildcard or use empty string
|
|
3307
|
+
const wildcard = params.wildcard || '';
|
|
3308
|
+
// Create base object with all identifier fields set to null
|
|
3309
|
+
const identifierObject = {
|
|
3310
|
+
type: 'identifierObject',
|
|
3311
|
+
id: null,
|
|
3312
|
+
email: null,
|
|
3313
|
+
instagramAccountId: null,
|
|
3314
|
+
externalId: null,
|
|
3315
|
+
identifierEmail: null,
|
|
3316
|
+
wildcard
|
|
3317
|
+
};
|
|
3318
|
+
// Override nulls with any provided values
|
|
3319
|
+
uniqueUserIdentifierKeys.forEach(key => {
|
|
3320
|
+
if (params[key]) {
|
|
3321
|
+
identifierObject[key] = params[key];
|
|
3322
|
+
}
|
|
3323
|
+
});
|
|
3324
|
+
// If wildcard is provided but no specific fields, auto-detect the field type
|
|
3325
|
+
if (wildcard && !params.id && !params.email && !params.instagramAccountId && !params.externalId) {
|
|
3326
|
+
const detected = detectIdentifierType(wildcard);
|
|
3327
|
+
identifierObject[detected.type] = detected.value;
|
|
3328
|
+
}
|
|
3329
|
+
return identifierObject;
|
|
3330
|
+
};
|
|
3331
|
+
|
|
3332
|
+
/**
|
|
3333
|
+
* Validation Types
|
|
3334
|
+
* Shared type definitions for field validation.
|
|
3335
|
+
*/
|
|
3336
|
+
// ============================================================================
|
|
3337
|
+
// Operator Text Mapping
|
|
3338
|
+
// ============================================================================
|
|
3339
|
+
const operatorText = {
|
|
3340
|
+
'<': 'less than',
|
|
3341
|
+
'<=': 'less than or equal to',
|
|
3342
|
+
'>': 'greater than',
|
|
3343
|
+
'>=': 'greater than or equal to',
|
|
3344
|
+
'==': 'equal to',
|
|
3345
|
+
'!=': 'not equal to',
|
|
3346
|
+
};
|
|
3347
|
+
|
|
3348
|
+
/**
|
|
3349
|
+
* Standalone Validators
|
|
3350
|
+
* Pure validation functions that can be used independently.
|
|
3351
|
+
* Shared between backend and frontend.
|
|
3352
|
+
*/
|
|
3353
|
+
// ============================================================================
|
|
3354
|
+
// Regex Patterns
|
|
3355
|
+
// ============================================================================
|
|
3356
|
+
/** RFC-compliant email regex */
|
|
3357
|
+
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
3358
|
+
const EMAIL_MAX_LENGTH = 254; // RFC 5321
|
|
3359
|
+
/** Basic URL pattern - http(s) with domain */
|
|
3360
|
+
const URL_REGEX = /^https?:\/\/[^\s/$.?#].[^\s]*$/i;
|
|
3361
|
+
/** Ethereum address format: 0x followed by 40 hexadecimal characters */
|
|
3362
|
+
const WALLET_ADDRESS_REGEX = /^0x[a-fA-F0-9]{40}$/;
|
|
3363
|
+
/** UUID v4 format */
|
|
3364
|
+
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
3365
|
+
// ============================================================================
|
|
3366
|
+
// Validators
|
|
3367
|
+
// ============================================================================
|
|
3368
|
+
/**
|
|
3369
|
+
* Validates email format according to RFC standards.
|
|
3370
|
+
* RFC 5321 length limit (254 characters) enforced.
|
|
3371
|
+
*/
|
|
3372
|
+
function isValidEmail(email) {
|
|
3373
|
+
if (!email || email.length > EMAIL_MAX_LENGTH)
|
|
3374
|
+
return false;
|
|
3375
|
+
return EMAIL_REGEX.test(email);
|
|
3376
|
+
}
|
|
3377
|
+
/**
|
|
3378
|
+
* Validates URL format (http/https only).
|
|
3379
|
+
*/
|
|
3380
|
+
function isValidUrl(url) {
|
|
3381
|
+
if (!url)
|
|
3382
|
+
return false;
|
|
3383
|
+
return URL_REGEX.test(url);
|
|
3384
|
+
}
|
|
3385
|
+
/**
|
|
3386
|
+
* Validates Ethereum wallet address format.
|
|
3387
|
+
* Format: 0x followed by 40 hexadecimal characters.
|
|
3388
|
+
*/
|
|
3389
|
+
function isValidWalletAddress(address) {
|
|
3390
|
+
if (!address)
|
|
3391
|
+
return false;
|
|
3392
|
+
return WALLET_ADDRESS_REGEX.test(address);
|
|
3393
|
+
}
|
|
3394
|
+
/**
|
|
3395
|
+
* Validates that a string is not empty and meets minimum length.
|
|
3396
|
+
*/
|
|
3397
|
+
function isValidNonEmptyString(value, minLength = 1) {
|
|
3398
|
+
return typeof value === 'string' && value.trim().length >= minLength;
|
|
3399
|
+
}
|
|
3400
|
+
/**
|
|
3401
|
+
* Validates UUID v4 format.
|
|
3402
|
+
*/
|
|
3403
|
+
function isValidUuid(uuid) {
|
|
3404
|
+
if (!uuid)
|
|
3405
|
+
return false;
|
|
3406
|
+
return UUID_REGEX.test(uuid);
|
|
3407
|
+
}
|
|
3408
|
+
|
|
3409
|
+
// ============================================================================
|
|
3410
|
+
// Comparison Functions
|
|
3411
|
+
// ============================================================================
|
|
3412
|
+
/**
|
|
3413
|
+
* Compare two values using the specified operator.
|
|
3414
|
+
* Works with numbers, dates, and strings.
|
|
3415
|
+
*/
|
|
3416
|
+
const compareValues = (a, b, op) => {
|
|
3417
|
+
switch (op) {
|
|
3418
|
+
case '<': return a < b;
|
|
3419
|
+
case '<=': return a <= b;
|
|
3420
|
+
case '>': return a > b;
|
|
3421
|
+
case '>=': return a >= b;
|
|
3422
|
+
case '==': return a === b;
|
|
3423
|
+
case '!=': return a !== b;
|
|
3424
|
+
}
|
|
3425
|
+
};
|
|
3426
|
+
/** Get human-readable operator text for error messages. */
|
|
3427
|
+
const getOperatorText = (op) => operatorText[op];
|
|
3428
|
+
// ============================================================================
|
|
3429
|
+
// Single Field Validation
|
|
3430
|
+
// ============================================================================
|
|
3431
|
+
/**
|
|
3432
|
+
* Validate a single field value against validation rules.
|
|
3433
|
+
* Does not check cross-field comparisons (use validateFieldComparisons for that).
|
|
3434
|
+
*/
|
|
3435
|
+
const validateFieldValue = (value, rules, fieldType) => {
|
|
3436
|
+
// No rules = valid
|
|
3437
|
+
if (!rules) {
|
|
3438
|
+
return null;
|
|
3439
|
+
}
|
|
3440
|
+
const stringValue = value !== null && value !== undefined ? String(value) : '';
|
|
3441
|
+
const isEmpty = value === null || value === undefined || value === '';
|
|
3442
|
+
// Skip validation if empty - required-ness is determined by requiredUserFields
|
|
3443
|
+
// (the merged field on RedemptionDTO), not by a validation rule.
|
|
3444
|
+
if (isEmpty) {
|
|
3445
|
+
return null;
|
|
3446
|
+
}
|
|
3447
|
+
// Type-specific validation
|
|
3448
|
+
switch (fieldType) {
|
|
3449
|
+
case 'text':
|
|
3450
|
+
if (rules.minLength && stringValue.length < rules.minLength) {
|
|
3451
|
+
return { field: '', message: `Minimum ${rules.minLength} characters required`, code: 'minLength' };
|
|
3452
|
+
}
|
|
3453
|
+
if (rules.maxLength && stringValue.length > rules.maxLength) {
|
|
3454
|
+
return { field: '', message: `Maximum ${rules.maxLength} characters allowed`, code: 'maxLength' };
|
|
3455
|
+
}
|
|
3456
|
+
if (rules.pattern && !new RegExp(rules.pattern).test(stringValue)) {
|
|
3457
|
+
return { field: '', message: rules.patternMessage || 'Invalid format', code: 'pattern' };
|
|
3458
|
+
}
|
|
3459
|
+
break;
|
|
3460
|
+
case 'email':
|
|
3461
|
+
if (stringValue.length > EMAIL_MAX_LENGTH || !EMAIL_REGEX.test(stringValue)) {
|
|
3462
|
+
return { field: '', message: 'Invalid email format', code: 'email' };
|
|
3463
|
+
}
|
|
3464
|
+
if (rules.maxLength && stringValue.length > rules.maxLength) {
|
|
3465
|
+
return { field: '', message: `Maximum ${rules.maxLength} characters allowed`, code: 'maxLength' };
|
|
3466
|
+
}
|
|
3467
|
+
break;
|
|
3468
|
+
case 'url':
|
|
3469
|
+
if (!URL_REGEX.test(stringValue)) {
|
|
3470
|
+
return { field: '', message: 'Invalid URL format', code: 'url' };
|
|
3471
|
+
}
|
|
3472
|
+
if (rules.maxLength && stringValue.length > rules.maxLength) {
|
|
3473
|
+
return { field: '', message: `Maximum ${rules.maxLength} characters allowed`, code: 'maxLength' };
|
|
3474
|
+
}
|
|
3475
|
+
break;
|
|
3476
|
+
case 'number':
|
|
3477
|
+
const numValue = typeof value === 'number' ? value : parseFloat(stringValue);
|
|
3478
|
+
if (isNaN(numValue)) {
|
|
3479
|
+
return { field: '', message: 'Must be a valid number', code: 'type' };
|
|
3480
|
+
}
|
|
3481
|
+
if (rules.min !== undefined && numValue < rules.min) {
|
|
3482
|
+
return { field: '', message: `Minimum value is ${rules.min}`, code: 'min' };
|
|
3483
|
+
}
|
|
3484
|
+
if (rules.max !== undefined && numValue > rules.max) {
|
|
3485
|
+
return { field: '', message: `Maximum value is ${rules.max}`, code: 'max' };
|
|
3486
|
+
}
|
|
3487
|
+
break;
|
|
3488
|
+
case 'boolean':
|
|
3489
|
+
if (typeof value !== 'boolean' && stringValue !== 'true' && stringValue !== 'false') {
|
|
3490
|
+
return { field: '', message: 'Must be true or false', code: 'type' };
|
|
3491
|
+
}
|
|
3492
|
+
break;
|
|
3493
|
+
case 'date':
|
|
3494
|
+
const dateValue = new Date(stringValue);
|
|
3495
|
+
if (isNaN(dateValue.getTime())) {
|
|
3496
|
+
return { field: '', message: 'Must be a valid date', code: 'type' };
|
|
3497
|
+
}
|
|
3498
|
+
const now = new Date();
|
|
3499
|
+
if (rules.minDate) {
|
|
3500
|
+
const minDate = rules.minDate === 'now' ? now : new Date(rules.minDate);
|
|
3501
|
+
if (dateValue < minDate) {
|
|
3502
|
+
return {
|
|
3503
|
+
field: '',
|
|
3504
|
+
message: rules.minDate === 'now' ? 'Date must be in the future' : `Date must be after ${rules.minDate}`,
|
|
3505
|
+
code: 'minDate'
|
|
3506
|
+
};
|
|
3507
|
+
}
|
|
3508
|
+
}
|
|
3509
|
+
if (rules.maxDate) {
|
|
3510
|
+
const maxDate = rules.maxDate === 'now' ? now : new Date(rules.maxDate);
|
|
3511
|
+
if (dateValue > maxDate) {
|
|
3512
|
+
return {
|
|
3513
|
+
field: '',
|
|
3514
|
+
message: rules.maxDate === 'now' ? 'Date must be in the past' : `Date must be before ${rules.maxDate}`,
|
|
3515
|
+
code: 'maxDate'
|
|
3516
|
+
};
|
|
3517
|
+
}
|
|
3518
|
+
}
|
|
3519
|
+
break;
|
|
3520
|
+
case 'objectArray':
|
|
3521
|
+
if (!Array.isArray(value)) {
|
|
3522
|
+
return { field: '', message: 'Must be an array', code: 'type' };
|
|
3523
|
+
}
|
|
3524
|
+
if (rules.minItems !== undefined && value.length < rules.minItems) {
|
|
3525
|
+
return { field: '', message: `Minimum ${rules.minItems} items required`, code: 'minItems' };
|
|
3526
|
+
}
|
|
3527
|
+
if (rules.maxItems !== undefined && value.length > rules.maxItems) {
|
|
3528
|
+
return { field: '', message: `Maximum ${rules.maxItems} items allowed`, code: 'maxItems' };
|
|
3529
|
+
}
|
|
3530
|
+
break;
|
|
3531
|
+
}
|
|
3532
|
+
return null;
|
|
3533
|
+
};
|
|
3534
|
+
// ============================================================================
|
|
3535
|
+
// Cross-Field / Literal Comparison Validation
|
|
3536
|
+
// ============================================================================
|
|
3537
|
+
/** Type guard for field comparison */
|
|
3538
|
+
const isFieldComparison = (c) => 'field' in c && c.field !== undefined;
|
|
3539
|
+
/**
|
|
3540
|
+
* Validate comparison rules for a single field.
|
|
3541
|
+
* Supports comparing to another field OR a literal value.
|
|
3542
|
+
*/
|
|
3543
|
+
const validateFieldComparisons = (fieldKey, value, comparisons, allData, fieldType) => {
|
|
3544
|
+
if (!comparisons || comparisons.length === 0)
|
|
3545
|
+
return [];
|
|
3546
|
+
const errors = [];
|
|
3547
|
+
for (const comparison of comparisons) {
|
|
3548
|
+
// Skip if this field's value is empty
|
|
3549
|
+
if (value === null || value === undefined || value === '')
|
|
3550
|
+
continue;
|
|
3551
|
+
// Get the comparison target (field value or literal)
|
|
3552
|
+
const isField = isFieldComparison(comparison);
|
|
3553
|
+
const targetValue = isField ? allData[comparison.field] : comparison.value;
|
|
3554
|
+
const targetLabel = isField ? comparison.field : String(comparison.value);
|
|
3555
|
+
// Skip if comparing to field and that field is empty
|
|
3556
|
+
if (isField && (targetValue === null || targetValue === undefined || targetValue === ''))
|
|
3557
|
+
continue;
|
|
3558
|
+
let isValid = true;
|
|
3559
|
+
const { operator, message } = comparison;
|
|
3560
|
+
switch (fieldType) {
|
|
3561
|
+
case 'date': {
|
|
3562
|
+
const thisDate = new Date(String(value));
|
|
3563
|
+
const otherDate = new Date(String(targetValue));
|
|
3564
|
+
if (isNaN(thisDate.getTime()) || isNaN(otherDate.getTime()))
|
|
3565
|
+
continue;
|
|
3566
|
+
isValid = compareValues(thisDate.getTime(), otherDate.getTime(), operator);
|
|
3567
|
+
break;
|
|
3568
|
+
}
|
|
3569
|
+
case 'number': {
|
|
3570
|
+
const thisNum = typeof value === 'number' ? value : parseFloat(String(value));
|
|
3571
|
+
const otherNum = typeof targetValue === 'number' ? targetValue : parseFloat(String(targetValue));
|
|
3572
|
+
if (isNaN(thisNum) || isNaN(otherNum))
|
|
3573
|
+
continue;
|
|
3574
|
+
isValid = compareValues(thisNum, otherNum, operator);
|
|
3575
|
+
break;
|
|
3576
|
+
}
|
|
3577
|
+
case 'boolean': {
|
|
3578
|
+
const thisBool = typeof value === 'boolean' ? value : value === 'true';
|
|
3579
|
+
const otherBool = typeof targetValue === 'boolean' ? targetValue : targetValue === 'true';
|
|
3580
|
+
isValid = operator === '==' ? thisBool === otherBool :
|
|
3581
|
+
operator === '!=' ? thisBool !== otherBool : true;
|
|
3582
|
+
break;
|
|
3583
|
+
}
|
|
3584
|
+
default: {
|
|
3585
|
+
isValid = compareValues(String(value), String(targetValue), operator);
|
|
3586
|
+
break;
|
|
3587
|
+
}
|
|
3588
|
+
}
|
|
3589
|
+
if (!isValid) {
|
|
3590
|
+
errors.push({
|
|
3591
|
+
field: fieldKey,
|
|
3592
|
+
message: message || `Must be ${getOperatorText(operator)} ${targetLabel}`,
|
|
3593
|
+
code: `comparison:${operator}:${targetLabel}`,
|
|
3594
|
+
});
|
|
3595
|
+
}
|
|
3596
|
+
}
|
|
3597
|
+
return errors;
|
|
3598
|
+
};
|
|
3599
|
+
// ============================================================================
|
|
3600
|
+
// Object Array Item Validation
|
|
3601
|
+
// ============================================================================
|
|
3602
|
+
/**
|
|
3603
|
+
* Validate each item in an object array against the object schema.
|
|
3604
|
+
* Checks required properties, type validation, and validates each property.
|
|
3605
|
+
*
|
|
3606
|
+
* @param fieldKey - Parent field key (for error reporting)
|
|
3607
|
+
* @param items - Array of objects to validate
|
|
3608
|
+
* @param schema - Schema defining expected properties
|
|
3609
|
+
* @returns Array of validation errors with item index in field path (e.g., "contacts[0].phone")
|
|
3610
|
+
*/
|
|
3611
|
+
const validateObjectArrayItems = (fieldKey, items, schema) => {
|
|
3612
|
+
const errors = [];
|
|
3613
|
+
if (!Array.isArray(items))
|
|
3614
|
+
return errors;
|
|
3615
|
+
items.forEach((item, index) => {
|
|
3616
|
+
if (typeof item !== 'object' || item === null) {
|
|
3617
|
+
errors.push({
|
|
3618
|
+
field: `${fieldKey}[${index}]`,
|
|
3619
|
+
code: 'type',
|
|
3620
|
+
message: 'Item must be an object',
|
|
3621
|
+
});
|
|
3622
|
+
return;
|
|
3623
|
+
}
|
|
3624
|
+
const obj = item;
|
|
3625
|
+
// Check each schema property
|
|
3626
|
+
for (const prop of schema) {
|
|
3627
|
+
const value = obj[prop.key];
|
|
3628
|
+
const propPath = `${fieldKey}[${index}].${prop.key}`;
|
|
3629
|
+
const isEmpty = value === null || value === undefined || value === '';
|
|
3630
|
+
// Required check for nested properties (ObjectFieldSchema.required)
|
|
3631
|
+
if (prop.required && isEmpty) {
|
|
3632
|
+
errors.push({
|
|
3633
|
+
field: propPath,
|
|
3634
|
+
code: 'required',
|
|
3635
|
+
message: `${prop.label} is required`,
|
|
3636
|
+
});
|
|
3637
|
+
continue;
|
|
3638
|
+
}
|
|
3639
|
+
if (isEmpty)
|
|
3640
|
+
continue;
|
|
3641
|
+
// Convert ObjectFieldSchema inline rules to FieldValidationRules
|
|
3642
|
+
const rules = {
|
|
3643
|
+
min: prop.min,
|
|
3644
|
+
max: prop.max,
|
|
3645
|
+
minLength: prop.minLength,
|
|
3646
|
+
maxLength: prop.maxLength,
|
|
3647
|
+
pattern: prop.pattern,
|
|
3648
|
+
patternMessage: prop.patternMessage,
|
|
3649
|
+
};
|
|
3650
|
+
// Validate the property value
|
|
3651
|
+
const error = validateFieldValue(value, rules, prop.fieldType);
|
|
3652
|
+
if (error) {
|
|
3653
|
+
errors.push({ ...error, field: propPath });
|
|
3654
|
+
}
|
|
3655
|
+
}
|
|
3656
|
+
// Cross-property comparisons within the same object item
|
|
3657
|
+
// e.g., checkOutDate > checkInDate within each reservation
|
|
3658
|
+
for (const prop of schema) {
|
|
3659
|
+
const value = obj[prop.key];
|
|
3660
|
+
if (value === null || value === undefined || value === '')
|
|
3661
|
+
continue;
|
|
3662
|
+
// ObjectFieldSchema doesn't currently have comparisons, but we could
|
|
3663
|
+
// validate date fields against 'now' using minDate/maxDate if we extend the schema
|
|
3664
|
+
}
|
|
3665
|
+
});
|
|
3666
|
+
return errors;
|
|
3667
|
+
};
|
|
3668
|
+
// ============================================================================
|
|
3669
|
+
// Full Data Validation
|
|
3670
|
+
// ============================================================================
|
|
3671
|
+
/**
|
|
3672
|
+
* Validate all fields in a data object against their definitions.
|
|
3673
|
+
* Runs both single-field and comparison validations.
|
|
3674
|
+
*
|
|
3675
|
+
* @param data - The data object to validate
|
|
3676
|
+
* @param definitions - Field definitions with validation rules
|
|
3677
|
+
* @returns Array of all validation errors
|
|
3678
|
+
*/
|
|
3679
|
+
const validateAllFields = (data, definitions) => {
|
|
3680
|
+
const errors = [];
|
|
3681
|
+
const definitionMap = new Map(definitions.map(d => [d.key, d]));
|
|
3682
|
+
for (const [key, value] of Object.entries(data)) {
|
|
3683
|
+
const def = definitionMap.get(key);
|
|
3684
|
+
if (!def)
|
|
3685
|
+
continue;
|
|
3686
|
+
// Single-field validation
|
|
3687
|
+
const error = validateFieldValue(value, def.validation, def.fieldType);
|
|
3688
|
+
if (error) {
|
|
3689
|
+
errors.push({ ...error, field: key });
|
|
3690
|
+
}
|
|
3691
|
+
// Comparison validation
|
|
3692
|
+
if (def.validation?.comparisons) {
|
|
3693
|
+
const comparisonErrors = validateFieldComparisons(key, value, def.validation.comparisons, data, def.fieldType);
|
|
3694
|
+
errors.push(...comparisonErrors);
|
|
3695
|
+
}
|
|
3696
|
+
// Object array item validation (validate each item's properties against objectSchema)
|
|
3697
|
+
if (def.fieldType === 'objectArray' && def.objectSchema && Array.isArray(value)) {
|
|
3698
|
+
const itemErrors = validateObjectArrayItems(key, value, def.objectSchema);
|
|
3699
|
+
errors.push(...itemErrors);
|
|
3700
|
+
}
|
|
3701
|
+
}
|
|
3702
|
+
return errors;
|
|
3703
|
+
};
|
|
3704
|
+
|
|
3705
|
+
/**
|
|
3706
|
+
* Built-in user profile fields that can be required for redemptions.
|
|
3707
|
+
* These are a subset of UserModelField - excludes 'id' and 'identifierEmail'
|
|
3708
|
+
* since those are system-managed, not user-fillable profile fields.
|
|
3709
|
+
*
|
|
3710
|
+
* Used for:
|
|
3711
|
+
* - RedemptionType base required fields (RedemptionType.baseRequiredUserFields)
|
|
3712
|
+
* - Redemption specific required fields (Redemption.specificRequiredUserFields)
|
|
3713
|
+
* - User profile validation before redemption claims
|
|
3714
|
+
* - Form generation for missing field prompts
|
|
3715
|
+
*/
|
|
3716
|
+
// Fields that can be required for user profile validation (excludes system fields)
|
|
3717
|
+
const builtInUserFieldsArray = [
|
|
3718
|
+
'email',
|
|
3719
|
+
'firstName',
|
|
3720
|
+
'lastName',
|
|
3721
|
+
'externalId',
|
|
3722
|
+
'instagramAccountId',
|
|
3723
|
+
'googleAccountName',
|
|
3724
|
+
];
|
|
3725
|
+
// Export the array for iteration
|
|
3726
|
+
const builtInUserFields = [...builtInUserFieldsArray];
|
|
3727
|
+
/**
|
|
3728
|
+
* Static metadata mapping for all built-in user fields.
|
|
3729
|
+
* Used by validation services and UI form generation.
|
|
3730
|
+
*/
|
|
3731
|
+
const builtInUserFieldMetadata = {
|
|
3732
|
+
email: {
|
|
3733
|
+
label: 'Email Address',
|
|
3734
|
+
description: 'User email address',
|
|
3735
|
+
fieldType: 'email',
|
|
3736
|
+
},
|
|
3737
|
+
firstName: {
|
|
3738
|
+
label: 'First Name',
|
|
3739
|
+
description: 'User first name',
|
|
3740
|
+
fieldType: 'text',
|
|
3741
|
+
},
|
|
3742
|
+
lastName: {
|
|
3743
|
+
label: 'Last Name',
|
|
3744
|
+
description: 'User last name',
|
|
3745
|
+
fieldType: 'text',
|
|
3746
|
+
},
|
|
3747
|
+
externalId: {
|
|
3748
|
+
label: 'External ID',
|
|
3749
|
+
description: 'External identifier from third-party system',
|
|
3750
|
+
fieldType: 'text',
|
|
3751
|
+
},
|
|
3752
|
+
instagramAccountId: {
|
|
3753
|
+
label: 'Instagram Account',
|
|
3754
|
+
description: 'Instagram account ID',
|
|
3755
|
+
fieldType: 'text',
|
|
3756
|
+
},
|
|
3757
|
+
googleAccountName: {
|
|
3758
|
+
label: 'Google Account',
|
|
3759
|
+
description: 'Google account name',
|
|
3760
|
+
fieldType: 'text',
|
|
3761
|
+
},
|
|
3762
|
+
};
|
|
3763
|
+
/**
|
|
3764
|
+
* Check if a string is a valid built-in user field
|
|
3765
|
+
*/
|
|
3766
|
+
const isBuiltInUserField = (value) => {
|
|
3767
|
+
return builtInUserFields.includes(value);
|
|
3768
|
+
};
|
|
3769
|
+
const getBuiltInFieldDefinition = (field) => {
|
|
3770
|
+
if (!isBuiltInUserField(field)) {
|
|
3771
|
+
return null;
|
|
3772
|
+
}
|
|
3773
|
+
const metadata = builtInUserFieldMetadata[field];
|
|
3774
|
+
const sortOrder = builtInUserFields.indexOf(field);
|
|
3775
|
+
return {
|
|
3776
|
+
id: `builtin:${field}`,
|
|
3777
|
+
tenantId: null,
|
|
3778
|
+
entityType: 'user',
|
|
3779
|
+
key: field,
|
|
3780
|
+
label: metadata.label,
|
|
3781
|
+
description: metadata.description,
|
|
3782
|
+
fieldType: metadata.fieldType,
|
|
3783
|
+
objectSchema: null,
|
|
3784
|
+
selectOptions: null,
|
|
3785
|
+
selectOptionsSource: null,
|
|
3786
|
+
validation: null,
|
|
3787
|
+
sortOrder,
|
|
3788
|
+
createdAt: null,
|
|
3789
|
+
updatedAt: null,
|
|
3790
|
+
isBuiltIn: true,
|
|
3791
|
+
};
|
|
3792
|
+
};
|
|
3793
|
+
/**
|
|
3794
|
+
* Get all built-in field definitions as an array.
|
|
3795
|
+
* Useful for populating field definitions list.
|
|
3796
|
+
*/
|
|
3797
|
+
const getAllBuiltInFieldDefinitions = () => {
|
|
3798
|
+
return builtInUserFields.map(field => getBuiltInFieldDefinition(field));
|
|
3799
|
+
};
|
|
3800
|
+
/**
|
|
3801
|
+
* Entity types for custom fields and dynamic select options.
|
|
3802
|
+
* Used for both field definitions (entityType) and select option sources.
|
|
3803
|
+
* - 'user': User profile fields (stored in user.customData)
|
|
3804
|
+
* - 'business': Business profile fields (stored in business.customData)
|
|
3805
|
+
* - 'campaign': Campaign-specific custom fields
|
|
3806
|
+
* - 'redemption': Redemption-specific custom fields
|
|
3807
|
+
*/
|
|
3808
|
+
const customFieldEntityTypesArray = ['user', 'business', 'campaign', 'redemption'];
|
|
3809
|
+
/** All supported custom field entity types for iteration/validation */
|
|
3810
|
+
const customFieldEntityTypes = [...customFieldEntityTypesArray];
|
|
3811
|
+
|
|
3185
3812
|
/**
|
|
3186
3813
|
* Core Entity Types
|
|
3187
3814
|
*
|
|
@@ -3344,119 +3971,17 @@ function isPingMessage(msg) {
|
|
|
3344
3971
|
return msg.type === 'ping';
|
|
3345
3972
|
}
|
|
3346
3973
|
|
|
3347
|
-
const apiPublicKeyTestPrefix = 'pk_test_';
|
|
3348
|
-
const testnetPrefix = 'testnet-';
|
|
3349
|
-
const testnetShortPrefix = 'tn-';
|
|
3350
|
-
|
|
3351
|
-
// Define the keys as a constant array
|
|
3352
|
-
const uniqueUserIdentifierKeysArray = [
|
|
3353
|
-
'id',
|
|
3354
|
-
'email',
|
|
3355
|
-
'identifierEmail',
|
|
3356
|
-
'instagramAccountId',
|
|
3357
|
-
'externalId', // This is a special key that can be used to search all fields
|
|
3358
|
-
];
|
|
3359
|
-
// Export the array
|
|
3360
|
-
const uniqueUserIdentifierKeys = [...uniqueUserIdentifierKeysArray];
|
|
3361
|
-
// Define identifier type configurations
|
|
3362
|
-
const identifierTypes = {
|
|
3363
|
-
id: {
|
|
3364
|
-
key: 'id',
|
|
3365
|
-
detect: (value) => /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value), // UUID pattern
|
|
3366
|
-
process: (value) => value
|
|
3367
|
-
},
|
|
3368
|
-
email: {
|
|
3369
|
-
key: 'email',
|
|
3370
|
-
detect: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
|
|
3371
|
-
process: (value) => value
|
|
3372
|
-
},
|
|
3373
|
-
instagramAccountId: {
|
|
3374
|
-
key: 'instagramAccountId',
|
|
3375
|
-
detect: (value) => /^@/.test(value) || value.toLowerCase().includes('instagram'),
|
|
3376
|
-
process: (value) => value.replace(/^@/, '') // Remove @ if present for consistency
|
|
3377
|
-
},
|
|
3378
|
-
externalId: {
|
|
3379
|
-
key: 'externalId',
|
|
3380
|
-
detect: () => true, // Default fallback
|
|
3381
|
-
process: (value) => value
|
|
3382
|
-
}
|
|
3383
|
-
};
|
|
3384
|
-
/**
|
|
3385
|
-
* Register a new identifier type configuration
|
|
3386
|
-
* @param key The identifier key
|
|
3387
|
-
* @param config The identifier configuration (without the key)
|
|
3388
|
-
*/
|
|
3389
|
-
const registerIdentifierType = (key, config) => {
|
|
3390
|
-
if (!uniqueUserIdentifierKeysArray.includes(key)) {
|
|
3391
|
-
console.warn(`Warning: Registering identifier type '${key}' that is not in uniqueUserIdentifierKeys`);
|
|
3392
|
-
}
|
|
3393
|
-
identifierTypes[key] = {
|
|
3394
|
-
key,
|
|
3395
|
-
...config
|
|
3396
|
-
};
|
|
3397
|
-
};
|
|
3398
3974
|
/**
|
|
3399
|
-
*
|
|
3400
|
-
* @param wildcardValue The value to analyze
|
|
3401
|
-
* @returns Object containing the type and processed value
|
|
3975
|
+
* Check if a string matches UUID format
|
|
3402
3976
|
*/
|
|
3403
|
-
const
|
|
3404
|
-
|
|
3405
|
-
return { type: 'externalId', value: '' };
|
|
3406
|
-
}
|
|
3407
|
-
const value = String(wildcardValue);
|
|
3408
|
-
// Try each identifier type in priority order (id first to catch UUIDs)
|
|
3409
|
-
for (const type of ['id', 'email', 'instagramAccountId', 'externalId']) {
|
|
3410
|
-
const config = identifierTypes[type];
|
|
3411
|
-
if (config.detect(value)) {
|
|
3412
|
-
return { type, value: config.process(value) };
|
|
3413
|
-
}
|
|
3414
|
-
}
|
|
3415
|
-
// Default fallback should never be reached due to externalId's detect always returning true
|
|
3416
|
-
return { type: 'externalId', value };
|
|
3417
|
-
};
|
|
3418
|
-
/**
|
|
3419
|
-
* Checks if an object is a properly formed UserIdentifierObject
|
|
3420
|
-
* @param obj Object to check
|
|
3421
|
-
* @returns Boolean indicating if the object is a UserIdentifierObject
|
|
3422
|
-
*/
|
|
3423
|
-
const isUserIdentifierObject = (obj) => {
|
|
3424
|
-
return obj &&
|
|
3425
|
-
obj.type === 'identifierObject' &&
|
|
3426
|
-
uniqueUserIdentifierKeys.every(key => key in obj);
|
|
3427
|
-
};
|
|
3428
|
-
/**
|
|
3429
|
-
* Creates a UserIdentifierObject with all required fields, setting missing values to null
|
|
3430
|
-
* @param params Partial parameters for the identifier object
|
|
3431
|
-
* @returns A properly formed UserIdentifierObject
|
|
3432
|
-
*/
|
|
3433
|
-
const createUserIdentifierObject = (params) => {
|
|
3434
|
-
// Extract the wildcard or use empty string
|
|
3435
|
-
const wildcard = params.wildcard || '';
|
|
3436
|
-
// Create base object with all identifier fields set to null
|
|
3437
|
-
const identifierObject = {
|
|
3438
|
-
type: 'identifierObject',
|
|
3439
|
-
id: null,
|
|
3440
|
-
email: null,
|
|
3441
|
-
instagramAccountId: null,
|
|
3442
|
-
externalId: null,
|
|
3443
|
-
identifierEmail: null,
|
|
3444
|
-
wildcard
|
|
3445
|
-
};
|
|
3446
|
-
// Override nulls with any provided values
|
|
3447
|
-
uniqueUserIdentifierKeys.forEach(key => {
|
|
3448
|
-
if (params[key]) {
|
|
3449
|
-
identifierObject[key] = params[key];
|
|
3450
|
-
}
|
|
3451
|
-
});
|
|
3452
|
-
// If wildcard is provided but no specific fields, auto-detect the field type
|
|
3453
|
-
if (wildcard && !params.id && !params.email && !params.instagramAccountId && !params.externalId) {
|
|
3454
|
-
const detected = detectIdentifierType(wildcard);
|
|
3455
|
-
identifierObject[detected.type] = detected.value;
|
|
3456
|
-
}
|
|
3457
|
-
return identifierObject;
|
|
3977
|
+
const isUUID = (value) => {
|
|
3978
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
|
|
3458
3979
|
};
|
|
3459
3980
|
|
|
3981
|
+
const apiPublicKeyTestPrefix = 'pk_test_';
|
|
3982
|
+
const testnetPrefix = 'testnet-';
|
|
3983
|
+
const testnetShortPrefix = 'tn-';
|
|
3984
|
+
|
|
3460
3985
|
// Use this pattern:
|
|
3461
3986
|
const NativeTokenTypes = {
|
|
3462
3987
|
ERC20: 'ERC20', // default; should be used for fungible tokens eg. credit tokens; There should be only one active ERC20 token per programme
|
|
@@ -3557,7 +4082,8 @@ function isValidTransactionRelation(value) {
|
|
|
3557
4082
|
const VALID_CAMPAIGN_RELATIONS = [
|
|
3558
4083
|
'triggerSources',
|
|
3559
4084
|
'businesses',
|
|
3560
|
-
'claimCount'
|
|
4085
|
+
'claimCount',
|
|
4086
|
+
'ownerBusiness'
|
|
3561
4087
|
];
|
|
3562
4088
|
/**
|
|
3563
4089
|
* Type guard to validate campaign include relation at runtime
|
|
@@ -3595,7 +4121,9 @@ function isValidCampaignClaimRelation(value) {
|
|
|
3595
4121
|
* Used for runtime validation
|
|
3596
4122
|
*/
|
|
3597
4123
|
const VALID_REDEMPTION_RELATIONS = [
|
|
3598
|
-
'redeemCount'
|
|
4124
|
+
'redeemCount',
|
|
4125
|
+
'ownerBusiness',
|
|
4126
|
+
'requiredUserFieldDefinitions'
|
|
3599
4127
|
];
|
|
3600
4128
|
/**
|
|
3601
4129
|
* Type guard to validate redemption include relation at runtime
|
|
@@ -3653,6 +4181,26 @@ function isValidUserRelation(value) {
|
|
|
3653
4181
|
return VALID_USER_RELATIONS.includes(value);
|
|
3654
4182
|
}
|
|
3655
4183
|
|
|
4184
|
+
/**
|
|
4185
|
+
* Array of all valid token metadata include relations
|
|
4186
|
+
* Used for runtime validation
|
|
4187
|
+
*/
|
|
4188
|
+
const VALID_TOKEN_METADATA_RELATIONS = [
|
|
4189
|
+
'ownerBusiness',
|
|
4190
|
+
'mintCount',
|
|
4191
|
+
'burnCount',
|
|
4192
|
+
'token'
|
|
4193
|
+
];
|
|
4194
|
+
/**
|
|
4195
|
+
* Type guard to validate token metadata include relation at runtime
|
|
4196
|
+
*
|
|
4197
|
+
* @param value - String to validate
|
|
4198
|
+
* @returns True if value is a valid TokenMetadataIncludeRelation
|
|
4199
|
+
*/
|
|
4200
|
+
function isValidTokenMetadataRelation(value) {
|
|
4201
|
+
return VALID_TOKEN_METADATA_RELATIONS.includes(value);
|
|
4202
|
+
}
|
|
4203
|
+
|
|
3656
4204
|
/**
|
|
3657
4205
|
* Normalizes filter options to use new field names while accepting deprecated ones.
|
|
3658
4206
|
* Use at controller level to support both old and new API contracts.
|
|
@@ -3673,6 +4221,134 @@ function normalizeFilters(filters) {
|
|
|
3673
4221
|
};
|
|
3674
4222
|
}
|
|
3675
4223
|
|
|
4224
|
+
/**
|
|
4225
|
+
* Analytics Constants
|
|
4226
|
+
*
|
|
4227
|
+
* Shared definitions for consistent analytics across frontend and backend.
|
|
4228
|
+
*/
|
|
4229
|
+
/**
|
|
4230
|
+
* Active User Definition
|
|
4231
|
+
*
|
|
4232
|
+
* A user qualifies as "active" in a given period if they performed
|
|
4233
|
+
* any of the qualifying actions during that period.
|
|
4234
|
+
*
|
|
4235
|
+
* @example
|
|
4236
|
+
* // Display in UI
|
|
4237
|
+
* `${engagementRate}% engagement. ${ACTIVE_USER_DEFINITION.description}`
|
|
4238
|
+
*
|
|
4239
|
+
* // Result: "25.5% engagement. Active = sent transaction, claimed, or redeemed in period"
|
|
4240
|
+
*/
|
|
4241
|
+
const ACTIVE_USER_DEFINITION = {
|
|
4242
|
+
/** Actions that qualify a user as "active" */
|
|
4243
|
+
qualifyingActions: ['TRANSACTION_SEND', 'CLAIM_COMPLETED', 'REDEMPTION_COMPLETED'],
|
|
4244
|
+
/** Short description for UI display */
|
|
4245
|
+
description: 'Active = sent transaction, claimed, or redeemed in period',
|
|
4246
|
+
/** Detailed explanation for tooltips/documentation */
|
|
4247
|
+
details: {
|
|
4248
|
+
TRANSACTION_SEND: 'User initiated a transaction (spending, transferring, or purchasing)',
|
|
4249
|
+
CLAIM_COMPLETED: 'User successfully claimed a campaign reward',
|
|
4250
|
+
REDEMPTION_COMPLETED: 'User successfully redeemed an offer',
|
|
4251
|
+
},
|
|
4252
|
+
/** What does NOT count as active */
|
|
4253
|
+
excluded: [
|
|
4254
|
+
'Receiving tokens (passive)',
|
|
4255
|
+
'Failed or pending claims/redemptions',
|
|
4256
|
+
'System-generated transactions (REDEMPTION_RECEIVE)',
|
|
4257
|
+
],
|
|
4258
|
+
};
|
|
4259
|
+
/**
|
|
4260
|
+
* Engagement rate formula
|
|
4261
|
+
* engagementRate = (activeUsers / totalUsers) × 100
|
|
4262
|
+
*/
|
|
4263
|
+
const ENGAGEMENT_RATE_FORMULA = 'activeUsers / totalUsers × 100';
|
|
4264
|
+
/**
|
|
4265
|
+
* Transaction statuses that count for analytics
|
|
4266
|
+
*/
|
|
4267
|
+
const ANALYTICS_VALID_TRANSACTION_STATUSES = ['SUCCEEDED', 'BROADCASTED'];
|
|
4268
|
+
/**
|
|
4269
|
+
* Process statuses that count for analytics (claims, redemptions)
|
|
4270
|
+
*/
|
|
4271
|
+
const ANALYTICS_VALID_PROCESS_STATUS = 'COMPLETED';
|
|
4272
|
+
|
|
4273
|
+
/**
|
|
4274
|
+
* Image CDN Configuration
|
|
4275
|
+
*
|
|
4276
|
+
* Shared constants and types for Image CDN (CloudFront + Lambda@Edge)
|
|
4277
|
+
* Used by backend Lambda handler and frontend SDKs.
|
|
4278
|
+
*
|
|
4279
|
+
* @see documentation/api-docs/image-cdn.md
|
|
4280
|
+
*/
|
|
4281
|
+
// ============================================
|
|
4282
|
+
// CONSTANTS
|
|
4283
|
+
// ============================================
|
|
4284
|
+
/** Maximum output dimension when no preset specified */
|
|
4285
|
+
const IMAGE_CDN_MAX_DIMENSION = 2048;
|
|
4286
|
+
/** Default quality percentage */
|
|
4287
|
+
const IMAGE_CDN_DEFAULT_QUALITY = 80;
|
|
4288
|
+
/** Default fit mode */
|
|
4289
|
+
const IMAGE_CDN_DEFAULT_FIT = 'inside';
|
|
4290
|
+
/** Default format (auto-detect based on browser Accept header) */
|
|
4291
|
+
const IMAGE_CDN_DEFAULT_FORMAT = 'auto';
|
|
4292
|
+
/** Available presets with their dimensions */
|
|
4293
|
+
const IMAGE_CDN_PRESETS = {
|
|
4294
|
+
thumb: { width: 150, height: 150 },
|
|
4295
|
+
medium: { width: 600, height: 600 },
|
|
4296
|
+
large: { width: 1200, height: 1200 },
|
|
4297
|
+
hero: { width: 1920, height: 800 },
|
|
4298
|
+
avatar: { width: 200, height: 200 },
|
|
4299
|
+
card: { width: 400, height: 300 },
|
|
4300
|
+
};
|
|
4301
|
+
/** Supported input/output formats */
|
|
4302
|
+
const IMAGE_CDN_SUPPORTED_FORMATS = ['jpeg', 'jpg', 'png', 'webp', 'gif', 'avif'];
|
|
4303
|
+
/** Cache TTL in seconds (1 year) */
|
|
4304
|
+
const IMAGE_CDN_CACHE_TTL = 31536000;
|
|
4305
|
+
/** Maximum input file size in bytes (10MB) */
|
|
4306
|
+
const IMAGE_CDN_MAX_INPUT_SIZE = 10 * 1024 * 1024;
|
|
4307
|
+
// ============================================
|
|
4308
|
+
// HELPER FUNCTIONS
|
|
4309
|
+
// ============================================
|
|
4310
|
+
/**
|
|
4311
|
+
* Build CDN URL with options
|
|
4312
|
+
*
|
|
4313
|
+
* @example
|
|
4314
|
+
* buildImageCdnUrl('https://cdn.example.com/photo.jpg', { preset: 'thumb' })
|
|
4315
|
+
* // => 'https://cdn.example.com/photo.jpg?preset=thumb'
|
|
4316
|
+
*
|
|
4317
|
+
* buildImageCdnUrl('https://cdn.example.com/photo.jpg', { preset: 'large', q: 95 })
|
|
4318
|
+
* // => 'https://cdn.example.com/photo.jpg?preset=large&q=95'
|
|
4319
|
+
*/
|
|
4320
|
+
function buildImageCdnUrl(baseUrl, options = {}) {
|
|
4321
|
+
const params = new URLSearchParams();
|
|
4322
|
+
if (options.preset)
|
|
4323
|
+
params.set('preset', options.preset);
|
|
4324
|
+
if (options.q !== undefined)
|
|
4325
|
+
params.set('q', options.q.toString());
|
|
4326
|
+
if (options.f)
|
|
4327
|
+
params.set('f', options.f);
|
|
4328
|
+
if (options.fit)
|
|
4329
|
+
params.set('fit', options.fit);
|
|
4330
|
+
const query = params.toString();
|
|
4331
|
+
return query ? `${baseUrl}?${query}` : baseUrl;
|
|
4332
|
+
}
|
|
4333
|
+
/**
|
|
4334
|
+
* Check if a preset name is valid
|
|
4335
|
+
*/
|
|
4336
|
+
function isValidPreset(preset) {
|
|
4337
|
+
return preset in IMAGE_CDN_PRESETS;
|
|
4338
|
+
}
|
|
4339
|
+
/**
|
|
4340
|
+
* Get dimensions for a preset
|
|
4341
|
+
*/
|
|
4342
|
+
function getPresetDimensions(preset) {
|
|
4343
|
+
return IMAGE_CDN_PRESETS[preset];
|
|
4344
|
+
}
|
|
4345
|
+
/**
|
|
4346
|
+
* Check if a file extension is a supported image format
|
|
4347
|
+
*/
|
|
4348
|
+
function isSupportedImageFormat(extension) {
|
|
4349
|
+
return IMAGE_CDN_SUPPORTED_FORMATS.includes(extension.toLowerCase());
|
|
4350
|
+
}
|
|
4351
|
+
|
|
3676
4352
|
/**
|
|
3677
4353
|
* Chain Data Types
|
|
3678
4354
|
*
|
|
@@ -3807,6 +4483,12 @@ const CommonErrorCodes = {
|
|
|
3807
4483
|
FIREBASE_AUTH_ERROR: 'FIREBASE_AUTH_ERROR',
|
|
3808
4484
|
MULTIPLE_CONTEXT_SELECTION_REQUIRED: 'MULTIPLE_CONTEXT_SELECTION_REQUIRED',
|
|
3809
4485
|
ADMIN_TENANT_REQUIRED: 'ADMIN_TENANT_REQUIRED',
|
|
4486
|
+
// Magic link errors
|
|
4487
|
+
MAGIC_LINK_EMAIL_SEND_FAILED: 'MAGIC_LINK_EMAIL_SEND_FAILED',
|
|
4488
|
+
MAGIC_LINK_EMAIL_MISMATCH: 'MAGIC_LINK_EMAIL_MISMATCH',
|
|
4489
|
+
MAGIC_LINK_VERIFICATION_FAILED: 'MAGIC_LINK_VERIFICATION_FAILED',
|
|
4490
|
+
// Rate limiting (generic)
|
|
4491
|
+
RATE_LIMITED: 'RATE_LIMITED',
|
|
3810
4492
|
// Generic fallbacks
|
|
3811
4493
|
INTERNAL_ERROR: 'INTERNAL_ERROR',
|
|
3812
4494
|
VALIDATION_ERROR: 'VALIDATION_ERROR',
|
|
@@ -5166,30 +5848,30 @@ class TokenApi {
|
|
|
5166
5848
|
return this.apiClient.get(url);
|
|
5167
5849
|
}
|
|
5168
5850
|
/**
|
|
5169
|
-
* Get all token metadata with filtering and
|
|
5851
|
+
* Get all token metadata with filtering, pagination, and include relations
|
|
5170
5852
|
*
|
|
5171
5853
|
* Useful for displaying rewards (ERC1155) or stamps (ERC721) in tables.
|
|
5172
5854
|
* Non-admin users always get active metadata only.
|
|
5173
5855
|
*
|
|
5174
|
-
* @param options - Filter and
|
|
5856
|
+
* @param options - Filter, pagination, and include options
|
|
5175
5857
|
* @returns Paginated response with token metadata
|
|
5176
5858
|
*
|
|
5177
|
-
* @example Get all active rewards
|
|
5859
|
+
* @example Get all active rewards with mint/burn counts
|
|
5178
5860
|
* ```typescript
|
|
5179
5861
|
* const rewards = await tokenApi.getTokenMetadata({
|
|
5180
5862
|
* tokenType: 'ERC1155',
|
|
5181
5863
|
* active: true,
|
|
5182
|
-
* limit: 10
|
|
5864
|
+
* limit: 10,
|
|
5865
|
+
* include: ['mintCount', 'burnCount', 'token']
|
|
5183
5866
|
* });
|
|
5184
5867
|
* ```
|
|
5185
5868
|
*
|
|
5186
|
-
* @example Get stamps with
|
|
5869
|
+
* @example Get stamps with owner business included
|
|
5187
5870
|
* ```typescript
|
|
5188
5871
|
* const stamps = await tokenApi.getTokenMetadata({
|
|
5189
5872
|
* tokenType: 'ERC721',
|
|
5190
5873
|
* search: 'gold',
|
|
5191
|
-
*
|
|
5192
|
-
* sortOrder: 'ASC'
|
|
5874
|
+
* include: ['ownerBusiness', 'token']
|
|
5193
5875
|
* });
|
|
5194
5876
|
* ```
|
|
5195
5877
|
*/
|
|
@@ -5214,6 +5896,14 @@ class TokenApi {
|
|
|
5214
5896
|
params.set('tags', tagsArray.join(','));
|
|
5215
5897
|
}
|
|
5216
5898
|
}
|
|
5899
|
+
// Filter by owner business
|
|
5900
|
+
if (options?.ownerBusinessId) {
|
|
5901
|
+
params.set('ownerBusinessId', options.ownerBusinessId);
|
|
5902
|
+
}
|
|
5903
|
+
// Include relations (ownerBusiness, mintCount, burnCount, token)
|
|
5904
|
+
if (options?.include && options.include.length > 0) {
|
|
5905
|
+
params.set('include', options.include.join(','));
|
|
5906
|
+
}
|
|
5217
5907
|
const response = await this.apiClient.get(`${this.basePath}/metadata?${params.toString()}`);
|
|
5218
5908
|
return normalizeToPaginated(response);
|
|
5219
5909
|
}
|
|
@@ -5273,6 +5963,30 @@ class TokenApi {
|
|
|
5273
5963
|
async createTokenType(tokenType) {
|
|
5274
5964
|
return this.apiClient.post(`${this.basePath}/types`, tokenType);
|
|
5275
5965
|
}
|
|
5966
|
+
/**
|
|
5967
|
+
* ADMIN: Delete token metadata (soft delete)
|
|
5968
|
+
*
|
|
5969
|
+
* Soft deletes token metadata by setting deletedAt timestamp.
|
|
5970
|
+
* Will fail with 409 Conflict if metadata is in use by active campaigns or redemptions.
|
|
5971
|
+
*
|
|
5972
|
+
* @param metadataId - ID of the token metadata to delete
|
|
5973
|
+
* @throws {PersApiError} 409 Conflict if metadata is in use
|
|
5974
|
+
*
|
|
5975
|
+
* @example
|
|
5976
|
+
* ```typescript
|
|
5977
|
+
* try {
|
|
5978
|
+
* await tokenApi.deleteTokenMetadata('metadata-123');
|
|
5979
|
+
* console.log('Metadata deleted');
|
|
5980
|
+
* } catch (error) {
|
|
5981
|
+
* if (error.statusCode === 409) {
|
|
5982
|
+
* console.log('Cannot delete: metadata is in use');
|
|
5983
|
+
* }
|
|
5984
|
+
* }
|
|
5985
|
+
* ```
|
|
5986
|
+
*/
|
|
5987
|
+
async deleteTokenMetadata(metadataId) {
|
|
5988
|
+
return this.apiClient.delete(`${this.basePath}/metadata/${metadataId}`);
|
|
5989
|
+
}
|
|
5276
5990
|
}
|
|
5277
5991
|
|
|
5278
5992
|
/**
|
|
@@ -5327,35 +6041,57 @@ class TokenService {
|
|
|
5327
6041
|
return this.tokenApi.getTokenByContractAddress(contractAddress, contractTokenId);
|
|
5328
6042
|
}
|
|
5329
6043
|
/**
|
|
5330
|
-
* Get all token metadata with filtering and
|
|
6044
|
+
* Get all token metadata with filtering, pagination, and include relations
|
|
5331
6045
|
*
|
|
5332
6046
|
* Useful for displaying rewards (ERC1155) or stamps (ERC721) in tables.
|
|
5333
6047
|
* Non-admin users always get active metadata only.
|
|
5334
6048
|
*
|
|
5335
|
-
* @param options - Filter and
|
|
6049
|
+
* @param options - Filter, pagination, and include options
|
|
5336
6050
|
* @returns Paginated response with token metadata
|
|
6051
|
+
*
|
|
6052
|
+
* @example Get rewards with counts
|
|
6053
|
+
* ```typescript
|
|
6054
|
+
* const rewards = await tokenService.getTokenMetadata({
|
|
6055
|
+
* tokenType: 'ERC1155',
|
|
6056
|
+
* include: ['mintCount', 'burnCount', 'token']
|
|
6057
|
+
* });
|
|
6058
|
+
* ```
|
|
5337
6059
|
*/
|
|
5338
6060
|
async getTokenMetadata(options) {
|
|
5339
6061
|
return this.tokenApi.getTokenMetadata(options);
|
|
5340
6062
|
}
|
|
5341
6063
|
/**
|
|
5342
|
-
* Get rewards (ERC1155 token metadata) with filtering and
|
|
6064
|
+
* Get rewards (ERC1155 token metadata) with filtering, pagination, and include relations
|
|
5343
6065
|
*
|
|
5344
6066
|
* Convenience method for fetching reward metadata.
|
|
5345
6067
|
*
|
|
5346
|
-
* @param options - Filter and
|
|
6068
|
+
* @param options - Filter, pagination, and include options (tokenType is set automatically)
|
|
5347
6069
|
* @returns Paginated response with reward metadata
|
|
6070
|
+
*
|
|
6071
|
+
* @example Get rewards with mint/burn counts and token info
|
|
6072
|
+
* ```typescript
|
|
6073
|
+
* const rewards = await tokenService.getRewards({
|
|
6074
|
+
* include: ['mintCount', 'burnCount', 'token', 'ownerBusiness']
|
|
6075
|
+
* });
|
|
6076
|
+
* ```
|
|
5348
6077
|
*/
|
|
5349
6078
|
async getRewards(options) {
|
|
5350
6079
|
return this.tokenApi.getTokenMetadata({ ...options, tokenType: NativeTokenTypes.ERC1155 });
|
|
5351
6080
|
}
|
|
5352
6081
|
/**
|
|
5353
|
-
* Get stamps (ERC721 token metadata) with filtering and
|
|
6082
|
+
* Get stamps (ERC721 token metadata) with filtering, pagination, and include relations
|
|
5354
6083
|
*
|
|
5355
6084
|
* Convenience method for fetching stamp metadata.
|
|
5356
6085
|
*
|
|
5357
|
-
* @param options - Filter and
|
|
6086
|
+
* @param options - Filter, pagination, and include options (tokenType is set automatically)
|
|
5358
6087
|
* @returns Paginated response with stamp metadata
|
|
6088
|
+
*
|
|
6089
|
+
* @example Get stamps with mint/burn counts and token info
|
|
6090
|
+
* ```typescript
|
|
6091
|
+
* const stamps = await tokenService.getStamps({
|
|
6092
|
+
* include: ['mintCount', 'burnCount', 'token', 'ownerBusiness']
|
|
6093
|
+
* });
|
|
6094
|
+
* ```
|
|
5359
6095
|
*/
|
|
5360
6096
|
async getStamps(options) {
|
|
5361
6097
|
return this.tokenApi.getTokenMetadata({ ...options, tokenType: NativeTokenTypes.ERC721 });
|
|
@@ -5412,6 +6148,17 @@ class TokenService {
|
|
|
5412
6148
|
async createTokenType(tokenType) {
|
|
5413
6149
|
return this.tokenApi.createTokenType(tokenType);
|
|
5414
6150
|
}
|
|
6151
|
+
/**
|
|
6152
|
+
* ADMIN: Delete token metadata (soft delete)
|
|
6153
|
+
*
|
|
6154
|
+
* Soft deletes token metadata. Will fail if metadata is in use.
|
|
6155
|
+
*
|
|
6156
|
+
* @param metadataId - ID of the token metadata to delete
|
|
6157
|
+
* @throws {PersApiError} 409 Conflict if metadata is in use by campaigns/redemptions
|
|
6158
|
+
*/
|
|
6159
|
+
async deleteTokenMetadata(metadataId) {
|
|
6160
|
+
return this.tokenApi.deleteTokenMetadata(metadataId);
|
|
6161
|
+
}
|
|
5415
6162
|
}
|
|
5416
6163
|
|
|
5417
6164
|
/**
|
|
@@ -5514,6 +6261,10 @@ class BusinessApi {
|
|
|
5514
6261
|
*
|
|
5515
6262
|
* // Filter by business type
|
|
5516
6263
|
* const hotels = await businessApi.getBusinesses({ businessTypeId: 1 });
|
|
6264
|
+
*
|
|
6265
|
+
* // Filter by location (geocoding)
|
|
6266
|
+
* const dohaBusinesses = await businessApi.getBusinesses({ city: 'Doha', countryCode: 'QA' });
|
|
6267
|
+
* const westBay = await businessApi.getBusinesses({ neighborhood: 'West Bay' });
|
|
5517
6268
|
* ```
|
|
5518
6269
|
*/
|
|
5519
6270
|
async getBusinesses(options) {
|
|
@@ -5536,6 +6287,22 @@ class BusinessApi {
|
|
|
5536
6287
|
if (options?.businessTypeId !== undefined) {
|
|
5537
6288
|
params.set('businessTypeId', String(options.businessTypeId));
|
|
5538
6289
|
}
|
|
6290
|
+
// Location filter options (geocoding)
|
|
6291
|
+
if (options?.city) {
|
|
6292
|
+
params.set('city', options.city);
|
|
6293
|
+
}
|
|
6294
|
+
if (options?.countryCode) {
|
|
6295
|
+
params.set('countryCode', options.countryCode);
|
|
6296
|
+
}
|
|
6297
|
+
if (options?.country) {
|
|
6298
|
+
params.set('country', options.country);
|
|
6299
|
+
}
|
|
6300
|
+
if (options?.neighborhood) {
|
|
6301
|
+
params.set('neighborhood', options.neighborhood);
|
|
6302
|
+
}
|
|
6303
|
+
if (options?.district) {
|
|
6304
|
+
params.set('district', options.district);
|
|
6305
|
+
}
|
|
5539
6306
|
const response = await this.apiClient.get(`${this.basePath}?${params.toString()}`);
|
|
5540
6307
|
return normalizeToPaginated(response);
|
|
5541
6308
|
}
|
|
@@ -7237,6 +8004,12 @@ class RedemptionService {
|
|
|
7237
8004
|
async updateRedemptionType(data) {
|
|
7238
8005
|
return this.redemptionApi.updateRedemptionType(data);
|
|
7239
8006
|
}
|
|
8007
|
+
/**
|
|
8008
|
+
* ADMIN: Delete redemption
|
|
8009
|
+
*/
|
|
8010
|
+
async deleteRedemption(id) {
|
|
8011
|
+
return this.redemptionApi.deleteRedemption(id);
|
|
8012
|
+
}
|
|
7240
8013
|
/**
|
|
7241
8014
|
* ADMIN: Delete redemption type
|
|
7242
8015
|
*/
|
|
@@ -9172,7 +9945,7 @@ class TriggerSourceApi {
|
|
|
9172
9945
|
/**
|
|
9173
9946
|
* PUBLIC: Get trigger sources with optional filters and pagination
|
|
9174
9947
|
*
|
|
9175
|
-
* @param options - Filter and pagination options
|
|
9948
|
+
* @param options - Filter and pagination options (includes location filters)
|
|
9176
9949
|
* @returns Paginated list of trigger sources
|
|
9177
9950
|
*
|
|
9178
9951
|
* @example
|
|
@@ -9186,6 +9959,12 @@ class TriggerSourceApi {
|
|
|
9186
9959
|
* page: 1,
|
|
9187
9960
|
* limit: 20
|
|
9188
9961
|
* });
|
|
9962
|
+
*
|
|
9963
|
+
* // Filter by location (geocoding)
|
|
9964
|
+
* const dubaiSources = await triggerSourceApi.getTriggerSources({
|
|
9965
|
+
* city: 'Dubai',
|
|
9966
|
+
* countryCode: 'AE'
|
|
9967
|
+
* });
|
|
9189
9968
|
* ```
|
|
9190
9969
|
*/
|
|
9191
9970
|
async getTriggerSources(options) {
|
|
@@ -9198,6 +9977,19 @@ class TriggerSourceApi {
|
|
|
9198
9977
|
params.set('campaignId', options.campaignId);
|
|
9199
9978
|
if (options?.active !== undefined)
|
|
9200
9979
|
params.set('active', String(options.active));
|
|
9980
|
+
if (options?.includeDeleted)
|
|
9981
|
+
params.set('includeDeleted', String(options.includeDeleted));
|
|
9982
|
+
// Location filter options (geocoding)
|
|
9983
|
+
if (options?.city)
|
|
9984
|
+
params.set('city', options.city);
|
|
9985
|
+
if (options?.countryCode)
|
|
9986
|
+
params.set('countryCode', options.countryCode);
|
|
9987
|
+
if (options?.country)
|
|
9988
|
+
params.set('country', options.country);
|
|
9989
|
+
if (options?.neighborhood)
|
|
9990
|
+
params.set('neighborhood', options.neighborhood);
|
|
9991
|
+
if (options?.district)
|
|
9992
|
+
params.set('district', options.district);
|
|
9201
9993
|
const response = await this.apiClient.get(`/trigger-sources?${params.toString()}`);
|
|
9202
9994
|
return normalizeToPaginated(response);
|
|
9203
9995
|
}
|
|
@@ -9782,6 +10574,9 @@ class AuthService {
|
|
|
9782
10574
|
* Extracts context (tenantId/businessId) from the current refresh token to ensure
|
|
9783
10575
|
* the recovered session maintains the same context.
|
|
9784
10576
|
*
|
|
10577
|
+
* This is a PUBLIC method that can be called by TokenRefreshManager when internal
|
|
10578
|
+
* refresh fails, to try external (provider token) recovery before giving up.
|
|
10579
|
+
*
|
|
9785
10580
|
* @returns true if session was successfully recovered
|
|
9786
10581
|
*/
|
|
9787
10582
|
async attemptProviderTokenRecovery() {
|
|
@@ -9934,6 +10729,8 @@ class TokenRefreshManager {
|
|
|
9934
10729
|
// Retry configuration for transient failures
|
|
9935
10730
|
this.MAX_RETRY_ATTEMPTS = 3;
|
|
9936
10731
|
this.RETRY_DELAY_MS = 1000;
|
|
10732
|
+
// Active refresh promise for deduplication
|
|
10733
|
+
this.activeRefreshPromise = null;
|
|
9937
10734
|
}
|
|
9938
10735
|
/**
|
|
9939
10736
|
* Ensures the access token is valid, refreshing if needed.
|
|
@@ -10034,11 +10831,27 @@ class TokenRefreshManager {
|
|
|
10034
10831
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
10035
10832
|
}
|
|
10036
10833
|
/**
|
|
10037
|
-
*
|
|
10834
|
+
* Attempt token refresh: internal first, then provider token fallback.
|
|
10835
|
+
* DEDUPLICATES concurrent calls - all callers share the same promise.
|
|
10038
10836
|
*/
|
|
10039
10837
|
async attemptInternalRefresh() {
|
|
10040
|
-
|
|
10041
|
-
|
|
10838
|
+
if (this.activeRefreshPromise) {
|
|
10839
|
+
return this.activeRefreshPromise;
|
|
10840
|
+
}
|
|
10841
|
+
this.activeRefreshPromise = (async () => {
|
|
10842
|
+
const result = await this.attemptRefreshWithRetry();
|
|
10843
|
+
if (result.success)
|
|
10844
|
+
return result;
|
|
10845
|
+
// Internal failed - try provider token recovery
|
|
10846
|
+
const recovered = await this.authService.attemptProviderTokenRecovery();
|
|
10847
|
+
return recovered ? { success: true, retryable: false } : result;
|
|
10848
|
+
})();
|
|
10849
|
+
try {
|
|
10850
|
+
return await this.activeRefreshPromise;
|
|
10851
|
+
}
|
|
10852
|
+
finally {
|
|
10853
|
+
this.activeRefreshPromise = null;
|
|
10854
|
+
}
|
|
10042
10855
|
}
|
|
10043
10856
|
}
|
|
10044
10857
|
|
|
@@ -10844,7 +11657,7 @@ class DefaultAuthProvider {
|
|
|
10844
11657
|
/** SDK package name */
|
|
10845
11658
|
const SDK_NAME = "@explorins/pers-sdk";
|
|
10846
11659
|
/** SDK version - injected from package.json at build time */
|
|
10847
|
-
const SDK_VERSION = "2.1
|
|
11660
|
+
const SDK_VERSION = "2.2.0-alpha.1";
|
|
10848
11661
|
/** Full SDK identifier for headers */
|
|
10849
11662
|
const SDK_USER_AGENT = `${SDK_NAME}/${SDK_VERSION}`;
|
|
10850
11663
|
|
|
@@ -10954,12 +11767,16 @@ class PersApiClient {
|
|
|
10954
11767
|
}
|
|
10955
11768
|
const { retryCount = 0, responseType = 'json', bypassAuth = false } = options || {};
|
|
10956
11769
|
const url = `${this.apiRoot}${endpoint}`;
|
|
10957
|
-
//
|
|
11770
|
+
// TOKEN VALIDATION: Ensure token is valid before request
|
|
11771
|
+
// This blocks the request until token validation completes - prevents 401s from expired tokens
|
|
10958
11772
|
if (!bypassAuth && this.mergedConfig.authProvider && retryCount === 0) {
|
|
10959
|
-
|
|
10960
|
-
|
|
10961
|
-
|
|
10962
|
-
|
|
11773
|
+
try {
|
|
11774
|
+
await this.ensureValidToken();
|
|
11775
|
+
}
|
|
11776
|
+
catch (error) {
|
|
11777
|
+
console.debug('[PersApiClient] Token validation failed:', error);
|
|
11778
|
+
// Continue with request - it will fail with 401 and trigger retry logic
|
|
11779
|
+
}
|
|
10963
11780
|
}
|
|
10964
11781
|
const requestOptions = {
|
|
10965
11782
|
headers: await this.getHeaders(!bypassAuth, method, url),
|
|
@@ -10987,49 +11804,63 @@ class PersApiClient {
|
|
|
10987
11804
|
return result;
|
|
10988
11805
|
}
|
|
10989
11806
|
catch (error) {
|
|
10990
|
-
// Error handling - proactive token refresh should prevent most 401s
|
|
10991
11807
|
const status = ErrorUtils.getStatus(error);
|
|
10992
11808
|
const errorMessage = ErrorUtils.getMessage(error);
|
|
10993
|
-
// Handle 401 errors
|
|
11809
|
+
// Handle 401 Unauthorized errors
|
|
10994
11810
|
if (status === 401) {
|
|
10995
11811
|
const backendError = ErrorUtils.extractBackendErrorDetails(error);
|
|
10996
|
-
//
|
|
10997
|
-
//
|
|
10998
|
-
//
|
|
11812
|
+
// ===========================================
|
|
11813
|
+
// FATAL AUTH ERRORS → Immediate logout
|
|
11814
|
+
// ===========================================
|
|
11815
|
+
// Fatal codes: REFRESH_TOKEN_EXPIRED, TOKEN_REVOKED, INVALID_REFRESH_TOKEN, etc.
|
|
10999
11816
|
if (this.authService.isFatalAuthError(backendError.code)) {
|
|
11000
|
-
|
|
11817
|
+
console.warn(`[PersApiClient] Fatal auth error: ${backendError.code}`);
|
|
11001
11818
|
await this.authService.handleAuthFailure();
|
|
11002
11819
|
this.emitErrorEvent(backendError, errorMessage, endpoint, method, status, error);
|
|
11003
11820
|
throw new AuthenticationError(errorMessage, endpoint, method, backendError);
|
|
11004
11821
|
}
|
|
11005
|
-
//
|
|
11822
|
+
// ===========================================
|
|
11823
|
+
// REFRESH REQUEST FAILED → Logout (prevents infinite loop)
|
|
11824
|
+
// ===========================================
|
|
11006
11825
|
if (options?.isRefreshRequest) {
|
|
11007
|
-
|
|
11826
|
+
console.warn('[PersApiClient] Refresh request itself failed with 401');
|
|
11008
11827
|
await this.authService.handleAuthFailure();
|
|
11009
11828
|
this.emitErrorEvent(backendError, errorMessage, endpoint, method, status, error);
|
|
11010
11829
|
throw new AuthenticationError(errorMessage, endpoint, method, backendError);
|
|
11011
11830
|
}
|
|
11012
|
-
//
|
|
11831
|
+
// ===========================================
|
|
11832
|
+
// RETRYABLE 401 (TOKEN_EXPIRED) → Try refresh, then retry
|
|
11833
|
+
// ===========================================
|
|
11013
11834
|
if (retryCount === 0 && this.mergedConfig.authProvider) {
|
|
11014
|
-
|
|
11015
|
-
|
|
11016
|
-
|
|
11017
|
-
|
|
11018
|
-
|
|
11019
|
-
|
|
11020
|
-
|
|
11021
|
-
|
|
11022
|
-
|
|
11023
|
-
// Retry with new token
|
|
11024
|
-
return await this.request(method, endpoint, body, { ...options, retryCount: 1 });
|
|
11025
|
-
}
|
|
11835
|
+
const refreshResult = await this.refreshManager.attemptInternalRefresh();
|
|
11836
|
+
if (refreshResult.success) {
|
|
11837
|
+
// Refresh succeeded → retry request with new token
|
|
11838
|
+
this._events?.emitSuccess({
|
|
11839
|
+
type: 'token_refreshed',
|
|
11840
|
+
domain: 'authentication',
|
|
11841
|
+
userMessage: 'Authentication token refreshed',
|
|
11842
|
+
});
|
|
11843
|
+
return await this.request(method, endpoint, body, { ...options, retryCount: 1 });
|
|
11026
11844
|
}
|
|
11027
|
-
|
|
11028
|
-
|
|
11845
|
+
// Refresh failed - check if it's a definitive failure
|
|
11846
|
+
if (!refreshResult.retryable) {
|
|
11847
|
+
// Non-retryable failure (e.g., refresh token expired) → logout
|
|
11848
|
+
console.warn('[PersApiClient] Refresh failed with non-retryable error, logging out');
|
|
11849
|
+
await this.authService.handleAuthFailure();
|
|
11029
11850
|
}
|
|
11851
|
+
else {
|
|
11852
|
+
// Retryable failure (network error, etc.) → DON'T logout, let app retry later
|
|
11853
|
+
console.warn('[PersApiClient] Refresh failed with retryable error, NOT logging out');
|
|
11854
|
+
}
|
|
11855
|
+
this.emitErrorEvent(backendError, errorMessage, endpoint, method, status, error);
|
|
11856
|
+
throw new AuthenticationError(errorMessage, endpoint, method, backendError);
|
|
11030
11857
|
}
|
|
11031
|
-
//
|
|
11032
|
-
|
|
11858
|
+
// ===========================================
|
|
11859
|
+
// RETRY FAILED (retryCount > 0) → Something wrong, but DON'T logout
|
|
11860
|
+
// ===========================================
|
|
11861
|
+
// We already refreshed successfully, but retry still got 401
|
|
11862
|
+
// This is unexpected - don't logout, let app investigate
|
|
11863
|
+
console.warn('[PersApiClient] Retry with new token still got 401 - not logging out');
|
|
11033
11864
|
this.emitErrorEvent(backendError, errorMessage, endpoint, method, status, error);
|
|
11034
11865
|
throw new AuthenticationError(errorMessage, endpoint, method, backendError);
|
|
11035
11866
|
}
|
|
@@ -12542,10 +13373,11 @@ class UserStatusManager {
|
|
|
12542
13373
|
* ```
|
|
12543
13374
|
*/
|
|
12544
13375
|
class TokenManager {
|
|
12545
|
-
constructor(apiClient) {
|
|
13376
|
+
constructor(apiClient, eventEmitter) {
|
|
12546
13377
|
this.apiClient = apiClient;
|
|
12547
13378
|
const tokenApi = new TokenApi(apiClient);
|
|
12548
13379
|
this.tokenService = new TokenService(tokenApi);
|
|
13380
|
+
this.eventEmitter = eventEmitter;
|
|
12549
13381
|
}
|
|
12550
13382
|
/**
|
|
12551
13383
|
* Get all available tokens
|
|
@@ -12694,16 +13526,17 @@ class TokenManager {
|
|
|
12694
13526
|
return this.tokenService.getTokenByContractAddress(contractAddress, contractTokenId);
|
|
12695
13527
|
}
|
|
12696
13528
|
/**
|
|
12697
|
-
* Get all token metadata with filtering and
|
|
13529
|
+
* Get all token metadata with filtering, pagination, and include relations
|
|
12698
13530
|
*
|
|
12699
|
-
* Retrieves token metadata (rewards/stamps) with server-side filtering
|
|
13531
|
+
* Retrieves token metadata (rewards/stamps) with server-side filtering, pagination,
|
|
13532
|
+
* and optional include relations for additional data.
|
|
12700
13533
|
* Useful for displaying rewards (ERC1155) or stamps (ERC721) in admin tables.
|
|
12701
13534
|
* Non-admin users always get active metadata only.
|
|
12702
13535
|
*
|
|
12703
|
-
* @param options - Filter and
|
|
13536
|
+
* @param options - Filter, pagination, and include options
|
|
12704
13537
|
* @returns Promise resolving to paginated token metadata
|
|
12705
13538
|
*
|
|
12706
|
-
* @example Get paginated rewards
|
|
13539
|
+
* @example Get paginated rewards with counts
|
|
12707
13540
|
* ```typescript
|
|
12708
13541
|
* const rewards = await sdk.tokens.getTokenMetadata({
|
|
12709
13542
|
* tokenType: 'ERC1155',
|
|
@@ -12711,19 +13544,26 @@ class TokenManager {
|
|
|
12711
13544
|
* page: 1,
|
|
12712
13545
|
* limit: 10,
|
|
12713
13546
|
* sortBy: 'name',
|
|
12714
|
-
* sortOrder: 'ASC'
|
|
13547
|
+
* sortOrder: 'ASC',
|
|
13548
|
+
* include: ['mintCount', 'burnCount', 'token']
|
|
12715
13549
|
* });
|
|
12716
13550
|
*
|
|
12717
|
-
*
|
|
12718
|
-
*
|
|
13551
|
+
* rewards.data.forEach(r => {
|
|
13552
|
+
* console.log(`${r.name}: ${r.mintCount} minted, ${r.burnCount} burned`);
|
|
13553
|
+
* console.log(`Contract: ${r.included?.token?.contractAddress}`);
|
|
13554
|
+
* });
|
|
12719
13555
|
* ```
|
|
12720
13556
|
*
|
|
12721
|
-
* @example Get stamps with
|
|
13557
|
+
* @example Get stamps with owner business
|
|
12722
13558
|
* ```typescript
|
|
12723
13559
|
* const stamps = await sdk.tokens.getTokenMetadata({
|
|
12724
13560
|
* tokenType: 'ERC721',
|
|
12725
13561
|
* search: 'gold',
|
|
12726
|
-
*
|
|
13562
|
+
* include: ['ownerBusiness', 'token']
|
|
13563
|
+
* });
|
|
13564
|
+
*
|
|
13565
|
+
* stamps.data.forEach(s => {
|
|
13566
|
+
* console.log(`${s.name} owned by ${s.included?.ownerBusiness?.displayName}`);
|
|
12727
13567
|
* });
|
|
12728
13568
|
* ```
|
|
12729
13569
|
*/
|
|
@@ -12731,31 +13571,27 @@ class TokenManager {
|
|
|
12731
13571
|
return this.tokenService.getTokenMetadata(options);
|
|
12732
13572
|
}
|
|
12733
13573
|
/**
|
|
12734
|
-
* Get rewards (ERC1155 token metadata) with filtering and
|
|
13574
|
+
* Get rewards (ERC1155 token metadata) with filtering, pagination, and include relations
|
|
12735
13575
|
*
|
|
12736
13576
|
* Convenience method for fetching reward metadata. Rewards are ERC1155 tokens
|
|
12737
13577
|
* that represent collectible items, vouchers, or other redeemable rewards.
|
|
12738
13578
|
*
|
|
12739
|
-
* @param options - Filter and
|
|
13579
|
+
* @param options - Filter, pagination, and include options (tokenType is set automatically)
|
|
12740
13580
|
* @returns Promise resolving to paginated reward metadata
|
|
12741
13581
|
*
|
|
12742
|
-
* @example Get active rewards
|
|
13582
|
+
* @example Get active rewards with mint counts
|
|
12743
13583
|
* ```typescript
|
|
12744
13584
|
* const rewards = await sdk.tokens.getRewards({
|
|
12745
13585
|
* active: true,
|
|
12746
|
-
* limit: 10
|
|
13586
|
+
* limit: 10,
|
|
13587
|
+
* include: ['mintCount', 'burnCount', 'token', 'ownerBusiness']
|
|
12747
13588
|
* });
|
|
12748
13589
|
*
|
|
12749
|
-
*
|
|
12750
|
-
*
|
|
12751
|
-
*
|
|
12752
|
-
*
|
|
12753
|
-
*
|
|
12754
|
-
* ```typescript
|
|
12755
|
-
* const discountRewards = await sdk.tokens.getRewards({
|
|
12756
|
-
* search: 'discount',
|
|
12757
|
-
* sortBy: 'name',
|
|
12758
|
-
* sortOrder: 'ASC'
|
|
13590
|
+
* rewards.data.forEach(r => {
|
|
13591
|
+
* console.log(`${r.name}: ${r.mintCount ?? 0} minted`);
|
|
13592
|
+
* if (r.included?.token) {
|
|
13593
|
+
* console.log(` Chain: ${r.included.token.chainId}, Contract: ${r.included.token.contractAddress}`);
|
|
13594
|
+
* }
|
|
12759
13595
|
* });
|
|
12760
13596
|
* ```
|
|
12761
13597
|
*/
|
|
@@ -12763,31 +13599,27 @@ class TokenManager {
|
|
|
12763
13599
|
return this.tokenService.getRewards(options);
|
|
12764
13600
|
}
|
|
12765
13601
|
/**
|
|
12766
|
-
* Get stamps (ERC721 token metadata) with filtering and
|
|
13602
|
+
* Get stamps (ERC721 token metadata) with filtering, pagination, and include relations
|
|
12767
13603
|
*
|
|
12768
13604
|
* Convenience method for fetching stamp metadata. Stamps are ERC721 tokens
|
|
12769
13605
|
* that represent achievements, badges, or collectible status markers.
|
|
12770
13606
|
*
|
|
12771
|
-
* @param options - Filter and
|
|
13607
|
+
* @param options - Filter, pagination, and include options (tokenType is set automatically)
|
|
12772
13608
|
* @returns Promise resolving to paginated stamp metadata
|
|
12773
13609
|
*
|
|
12774
|
-
* @example Get active stamps
|
|
13610
|
+
* @example Get active stamps with all includes
|
|
12775
13611
|
* ```typescript
|
|
12776
13612
|
* const stamps = await sdk.tokens.getStamps({
|
|
12777
13613
|
* active: true,
|
|
12778
|
-
* limit: 10
|
|
13614
|
+
* limit: 10,
|
|
13615
|
+
* include: ['mintCount', 'burnCount', 'token', 'ownerBusiness']
|
|
12779
13616
|
* });
|
|
12780
13617
|
*
|
|
12781
|
-
*
|
|
12782
|
-
*
|
|
12783
|
-
*
|
|
12784
|
-
*
|
|
12785
|
-
*
|
|
12786
|
-
* ```typescript
|
|
12787
|
-
* const goldStamps = await sdk.tokens.getStamps({
|
|
12788
|
-
* tags: ['gold', 'premium'],
|
|
12789
|
-
* sortBy: 'createdAt',
|
|
12790
|
-
* sortOrder: 'DESC'
|
|
13618
|
+
* stamps.data.forEach(s => {
|
|
13619
|
+
* console.log(`${s.name}: ${s.mintCount ?? 0} minted`);
|
|
13620
|
+
* if (s.included?.ownerBusiness) {
|
|
13621
|
+
* console.log(` Owner: ${s.included.ownerBusiness.displayName}`);
|
|
13622
|
+
* }
|
|
12791
13623
|
* });
|
|
12792
13624
|
* ```
|
|
12793
13625
|
*/
|
|
@@ -12885,6 +13717,32 @@ class TokenManager {
|
|
|
12885
13717
|
async toggleTokenActive(tokenId) {
|
|
12886
13718
|
return this.tokenService.toggleTokenActive(tokenId);
|
|
12887
13719
|
}
|
|
13720
|
+
/**
|
|
13721
|
+
* Admin: Delete token metadata (soft delete)
|
|
13722
|
+
*
|
|
13723
|
+
* Soft deletes token metadata by setting deletedAt timestamp.
|
|
13724
|
+
* Requires administrator privileges. Will fail with 409 Conflict
|
|
13725
|
+
* if metadata is currently in use by active campaigns or redemptions.
|
|
13726
|
+
*
|
|
13727
|
+
* @param metadataId - ID of the token metadata to delete
|
|
13728
|
+
* @throws {PersApiError} When not authenticated as admin
|
|
13729
|
+
* @throws {PersApiError} 409 Conflict if metadata is in use
|
|
13730
|
+
*
|
|
13731
|
+
* @example
|
|
13732
|
+
* ```typescript
|
|
13733
|
+
* try {
|
|
13734
|
+
* await sdk.tokens.deleteTokenMetadata('metadata-123');
|
|
13735
|
+
* console.log('Token metadata deleted');
|
|
13736
|
+
* } catch (error) {
|
|
13737
|
+
* if (error.statusCode === 409) {
|
|
13738
|
+
* console.log('Cannot delete: metadata is used by campaigns/redemptions');
|
|
13739
|
+
* }
|
|
13740
|
+
* }
|
|
13741
|
+
* ```
|
|
13742
|
+
*/
|
|
13743
|
+
async deleteTokenMetadata(metadataId) {
|
|
13744
|
+
return this.tokenService.deleteTokenMetadata(metadataId);
|
|
13745
|
+
}
|
|
12888
13746
|
/**
|
|
12889
13747
|
* Get the full token SDK for advanced operations
|
|
12890
13748
|
*
|
|
@@ -15278,6 +16136,21 @@ class RedemptionManager {
|
|
|
15278
16136
|
async deleteRedemptionType(id) {
|
|
15279
16137
|
return this.redemptionService.deleteRedemptionType(id);
|
|
15280
16138
|
}
|
|
16139
|
+
/**
|
|
16140
|
+
* Delete a redemption (soft delete)
|
|
16141
|
+
*
|
|
16142
|
+
* @param id - Redemption ID to delete
|
|
16143
|
+
* @returns Promise resolving to true if deleted successfully
|
|
16144
|
+
*
|
|
16145
|
+
* @example
|
|
16146
|
+
* ```typescript
|
|
16147
|
+
* const success = await sdk.redemptions.deleteRedemption('redemption-123');
|
|
16148
|
+
* console.log('Deleted:', success);
|
|
16149
|
+
* ```
|
|
16150
|
+
*/
|
|
16151
|
+
async deleteRedemption(id) {
|
|
16152
|
+
return this.redemptionService.deleteRedemption(id);
|
|
16153
|
+
}
|
|
15281
16154
|
/**
|
|
15282
16155
|
* Get the full redemption service for advanced operations
|
|
15283
16156
|
*
|
|
@@ -19305,6 +20178,429 @@ class WalletEventsManager {
|
|
|
19305
20178
|
}
|
|
19306
20179
|
}
|
|
19307
20180
|
|
|
20181
|
+
/**
|
|
20182
|
+
* Custom Field Definitions API
|
|
20183
|
+
*
|
|
20184
|
+
* Low-level API for custom field definition CRUD operations.
|
|
20185
|
+
* Used internally by CustomFieldDefinitionManager.
|
|
20186
|
+
*
|
|
20187
|
+
* @internal
|
|
20188
|
+
*/
|
|
20189
|
+
class CustomFieldApi {
|
|
20190
|
+
constructor(apiClient) {
|
|
20191
|
+
this.apiClient = apiClient;
|
|
20192
|
+
this.basePath = '/custom-field-definitions';
|
|
20193
|
+
}
|
|
20194
|
+
/**
|
|
20195
|
+
* List all custom field definitions
|
|
20196
|
+
* Backend returns non-paginated array sorted by sortOrder
|
|
20197
|
+
*/
|
|
20198
|
+
async getDefinitions(entityType) {
|
|
20199
|
+
const params = new URLSearchParams();
|
|
20200
|
+
if (entityType) {
|
|
20201
|
+
params.append('entityType', entityType);
|
|
20202
|
+
}
|
|
20203
|
+
const query = params.toString();
|
|
20204
|
+
const url = query ? `${this.basePath}?${query}` : this.basePath;
|
|
20205
|
+
return this.apiClient.get(url);
|
|
20206
|
+
}
|
|
20207
|
+
/**
|
|
20208
|
+
* Get a single custom field definition by ID
|
|
20209
|
+
*/
|
|
20210
|
+
async getDefinition(id) {
|
|
20211
|
+
return this.apiClient.get(`${this.basePath}/${id}`);
|
|
20212
|
+
}
|
|
20213
|
+
/**
|
|
20214
|
+
* Create a new custom field definition
|
|
20215
|
+
*/
|
|
20216
|
+
async createDefinition(data) {
|
|
20217
|
+
return this.apiClient.post(this.basePath, data);
|
|
20218
|
+
}
|
|
20219
|
+
/**
|
|
20220
|
+
* Update an existing custom field definition
|
|
20221
|
+
*/
|
|
20222
|
+
async updateDefinition(id, data) {
|
|
20223
|
+
return this.apiClient.put(`${this.basePath}/${id}`, data);
|
|
20224
|
+
}
|
|
20225
|
+
/**
|
|
20226
|
+
* Delete a custom field definition
|
|
20227
|
+
*/
|
|
20228
|
+
async deleteDefinition(id) {
|
|
20229
|
+
return this.apiClient.delete(`${this.basePath}/${id}`);
|
|
20230
|
+
}
|
|
20231
|
+
/**
|
|
20232
|
+
* Get usage count for a field definition
|
|
20233
|
+
*/
|
|
20234
|
+
async getUsages(id) {
|
|
20235
|
+
return this.apiClient.get(`${this.basePath}/${id}/usages`);
|
|
20236
|
+
}
|
|
20237
|
+
/**
|
|
20238
|
+
* Resolve dynamic select options for a field
|
|
20239
|
+
*/
|
|
20240
|
+
async resolveSelectOptions(id) {
|
|
20241
|
+
return this.apiClient.get(`${this.basePath}/${id}/options`);
|
|
20242
|
+
}
|
|
20243
|
+
}
|
|
20244
|
+
|
|
20245
|
+
/**
|
|
20246
|
+
* Custom Field Definition Manager - Manage tenant-specific custom fields
|
|
20247
|
+
*
|
|
20248
|
+
* Provides CRUD operations for custom field definitions that extend the built-in
|
|
20249
|
+
* user profile fields. Custom fields are defined per-tenant and can be used for
|
|
20250
|
+
* additional user data collection, redemption requirements, and form validation.
|
|
20251
|
+
*
|
|
20252
|
+
* @group Managers
|
|
20253
|
+
* @category Custom Fields
|
|
20254
|
+
*
|
|
20255
|
+
* @example Basic Operations
|
|
20256
|
+
* ```typescript
|
|
20257
|
+
* // List all user custom fields
|
|
20258
|
+
* const fields = await sdk.customFields.getDefinitions();
|
|
20259
|
+
*
|
|
20260
|
+
* // Create a new custom field
|
|
20261
|
+
* const field = await sdk.customFields.createDefinition({
|
|
20262
|
+
* key: 'employee_id',
|
|
20263
|
+
* label: 'Employee ID',
|
|
20264
|
+
* fieldType: 'text',
|
|
20265
|
+
* validation: { required: true, pattern: '^E[0-9]{5}$' }
|
|
20266
|
+
* });
|
|
20267
|
+
*
|
|
20268
|
+
* // Update a field
|
|
20269
|
+
* await sdk.customFields.updateDefinition(field.id, {
|
|
20270
|
+
* label: 'Company Employee ID'
|
|
20271
|
+
* });
|
|
20272
|
+
*
|
|
20273
|
+
* // Delete a field
|
|
20274
|
+
* await sdk.customFields.deleteDefinition(field.id);
|
|
20275
|
+
* ```
|
|
20276
|
+
*
|
|
20277
|
+
* @example Validation
|
|
20278
|
+
* ```typescript
|
|
20279
|
+
* // Validate user custom data against definitions
|
|
20280
|
+
* const definitions = await sdk.customFields.getDefinitions();
|
|
20281
|
+
* const errors = sdk.customFields.validateUserData(
|
|
20282
|
+
* { employee_id: 'INVALID' },
|
|
20283
|
+
* definitions
|
|
20284
|
+
* );
|
|
20285
|
+
* if (errors.length > 0) {
|
|
20286
|
+
* console.log('Validation errors:', errors);
|
|
20287
|
+
* }
|
|
20288
|
+
* ```
|
|
20289
|
+
*/
|
|
20290
|
+
class CustomFieldDefinitionManager {
|
|
20291
|
+
constructor(apiClient, events) {
|
|
20292
|
+
this.events = events;
|
|
20293
|
+
this.api = new CustomFieldApi(apiClient);
|
|
20294
|
+
}
|
|
20295
|
+
/**
|
|
20296
|
+
* List all custom field definitions for the tenant
|
|
20297
|
+
*
|
|
20298
|
+
* Retrieves all custom field definitions, optionally filtered by entity type.
|
|
20299
|
+
* Results are sorted by sortOrder (ascending).
|
|
20300
|
+
*
|
|
20301
|
+
* @param options - Query options including entity type filter
|
|
20302
|
+
* @returns Array of custom field definitions
|
|
20303
|
+
*
|
|
20304
|
+
* @example List All User Fields
|
|
20305
|
+
* ```typescript
|
|
20306
|
+
* const fields = await sdk.customFields.getDefinitions();
|
|
20307
|
+
* console.log(`Found ${fields.length} custom fields`);
|
|
20308
|
+
*
|
|
20309
|
+
* fields.forEach(field => {
|
|
20310
|
+
* console.log(`${field.key}: ${field.label} (${field.fieldType})`);
|
|
20311
|
+
* });
|
|
20312
|
+
* ```
|
|
20313
|
+
*
|
|
20314
|
+
* @example Filter by Entity Type
|
|
20315
|
+
* ```typescript
|
|
20316
|
+
* // Get only business custom fields
|
|
20317
|
+
* const businessFields = await sdk.customFields.getDefinitions({
|
|
20318
|
+
* entityType: 'business'
|
|
20319
|
+
* });
|
|
20320
|
+
* ```
|
|
20321
|
+
*/
|
|
20322
|
+
async getDefinitions(options) {
|
|
20323
|
+
return this.api.getDefinitions(options?.entityType);
|
|
20324
|
+
}
|
|
20325
|
+
/**
|
|
20326
|
+
* Get a single custom field definition by ID
|
|
20327
|
+
*
|
|
20328
|
+
* @param id - The UUID of the custom field definition
|
|
20329
|
+
* @returns The custom field definition
|
|
20330
|
+
* @throws {PersApiError} When definition not found (404)
|
|
20331
|
+
*
|
|
20332
|
+
* @example
|
|
20333
|
+
* ```typescript
|
|
20334
|
+
* try {
|
|
20335
|
+
* const field = await sdk.customFields.getDefinition('uuid-here');
|
|
20336
|
+
* console.log('Field:', field.label);
|
|
20337
|
+
* } catch (error) {
|
|
20338
|
+
* if (error.statusCode === 404) {
|
|
20339
|
+
* console.log('Field not found');
|
|
20340
|
+
* }
|
|
20341
|
+
* }
|
|
20342
|
+
* ```
|
|
20343
|
+
*/
|
|
20344
|
+
async getDefinition(id) {
|
|
20345
|
+
return this.api.getDefinition(id);
|
|
20346
|
+
}
|
|
20347
|
+
/**
|
|
20348
|
+
* Create a new custom field definition
|
|
20349
|
+
*
|
|
20350
|
+
* Creates a new custom field for the tenant. The field key must be unique
|
|
20351
|
+
* within the tenant and entity type combination.
|
|
20352
|
+
*
|
|
20353
|
+
* @param data - The field definition data
|
|
20354
|
+
* @returns The created custom field definition
|
|
20355
|
+
* @throws {PersApiError} When key already exists (409) or validation fails (400)
|
|
20356
|
+
*
|
|
20357
|
+
* @example Text Field with Pattern
|
|
20358
|
+
* ```typescript
|
|
20359
|
+
* const employeeIdField = await sdk.customFields.createDefinition({
|
|
20360
|
+
* key: 'employee_id',
|
|
20361
|
+
* label: 'Employee ID',
|
|
20362
|
+
* description: 'Your company employee ID (E + 5 digits)',
|
|
20363
|
+
* fieldType: 'text',
|
|
20364
|
+
* validation: {
|
|
20365
|
+
* required: true,
|
|
20366
|
+
* pattern: '^E[0-9]{5}$',
|
|
20367
|
+
* patternMessage: 'Must be E followed by 5 digits'
|
|
20368
|
+
* },
|
|
20369
|
+
* sortOrder: 1
|
|
20370
|
+
* });
|
|
20371
|
+
* ```
|
|
20372
|
+
*
|
|
20373
|
+
* @example Date Field with Comparison
|
|
20374
|
+
* ```typescript
|
|
20375
|
+
* const checkOutField = await sdk.customFields.createDefinition({
|
|
20376
|
+
* key: 'check_out',
|
|
20377
|
+
* label: 'Check-out Date',
|
|
20378
|
+
* fieldType: 'date',
|
|
20379
|
+
* validation: {
|
|
20380
|
+
* required: true,
|
|
20381
|
+
* comparisons: [
|
|
20382
|
+
* { field: 'check_in', operator: '>', message: 'Must be after check-in' }
|
|
20383
|
+
* ]
|
|
20384
|
+
* },
|
|
20385
|
+
* sortOrder: 2
|
|
20386
|
+
* });
|
|
20387
|
+
* ```
|
|
20388
|
+
*
|
|
20389
|
+
* @example Select Field with Static Options
|
|
20390
|
+
* ```typescript
|
|
20391
|
+
* const departmentField = await sdk.customFields.createDefinition({
|
|
20392
|
+
* key: 'department',
|
|
20393
|
+
* label: 'Department',
|
|
20394
|
+
* fieldType: 'select',
|
|
20395
|
+
* selectOptions: [
|
|
20396
|
+
* { value: 'engineering', label: 'Engineering' },
|
|
20397
|
+
* { value: 'sales', label: 'Sales' },
|
|
20398
|
+
* { value: 'hr', label: 'Human Resources' }
|
|
20399
|
+
* ],
|
|
20400
|
+
* validation: { required: true }
|
|
20401
|
+
* });
|
|
20402
|
+
* ```
|
|
20403
|
+
*
|
|
20404
|
+
* @example Select Field with Dynamic Options
|
|
20405
|
+
* ```typescript
|
|
20406
|
+
* const hotelField = await sdk.customFields.createDefinition({
|
|
20407
|
+
* key: 'preferred_hotel',
|
|
20408
|
+
* label: 'Preferred Hotel',
|
|
20409
|
+
* fieldType: 'select',
|
|
20410
|
+
* selectOptionsSource: {
|
|
20411
|
+
* entity: 'business',
|
|
20412
|
+
* valueField: 'id',
|
|
20413
|
+
* labelField: 'name',
|
|
20414
|
+
* filter: { tags: ['hotel'], isActive: true }
|
|
20415
|
+
* },
|
|
20416
|
+
* validation: { required: true }
|
|
20417
|
+
* });
|
|
20418
|
+
* ```
|
|
20419
|
+
*/
|
|
20420
|
+
async createDefinition(data) {
|
|
20421
|
+
const result = await this.api.createDefinition(data);
|
|
20422
|
+
// Using 'user' domain until pers-shared adds 'customField' domain
|
|
20423
|
+
this.events?.emitSuccess({
|
|
20424
|
+
domain: 'user',
|
|
20425
|
+
type: 'definition_created',
|
|
20426
|
+
userMessage: `Custom field "${result.label}" created`,
|
|
20427
|
+
details: { id: result.id, key: result.key }
|
|
20428
|
+
});
|
|
20429
|
+
return result;
|
|
20430
|
+
}
|
|
20431
|
+
/**
|
|
20432
|
+
* Update an existing custom field definition
|
|
20433
|
+
*
|
|
20434
|
+
* Updates a custom field definition. Note that key and entityType cannot
|
|
20435
|
+
* be changed after creation.
|
|
20436
|
+
*
|
|
20437
|
+
* @param id - The UUID of the custom field definition to update
|
|
20438
|
+
* @param data - The fields to update (partial update supported)
|
|
20439
|
+
* @returns The updated custom field definition
|
|
20440
|
+
* @throws {PersApiError} When definition not found (404) or validation fails (400)
|
|
20441
|
+
*
|
|
20442
|
+
* @example Update Label and Validation
|
|
20443
|
+
* ```typescript
|
|
20444
|
+
* const updated = await sdk.customFields.updateDefinition('uuid-here', {
|
|
20445
|
+
* label: 'Employee ID (Required)',
|
|
20446
|
+
* validation: {
|
|
20447
|
+
* required: true,
|
|
20448
|
+
* minLength: 6,
|
|
20449
|
+
* maxLength: 6
|
|
20450
|
+
* }
|
|
20451
|
+
* });
|
|
20452
|
+
* ```
|
|
20453
|
+
*
|
|
20454
|
+
* @example Update Sort Order
|
|
20455
|
+
* ```typescript
|
|
20456
|
+
* await sdk.customFields.updateDefinition('uuid-here', {
|
|
20457
|
+
* sortOrder: 5
|
|
20458
|
+
* });
|
|
20459
|
+
* ```
|
|
20460
|
+
*/
|
|
20461
|
+
async updateDefinition(id, data) {
|
|
20462
|
+
const result = await this.api.updateDefinition(id, data);
|
|
20463
|
+
// Using 'user' domain until pers-shared adds 'customField' domain
|
|
20464
|
+
this.events?.emitSuccess({
|
|
20465
|
+
domain: 'user',
|
|
20466
|
+
type: 'definition_updated',
|
|
20467
|
+
userMessage: `Custom field "${result.label}" updated`,
|
|
20468
|
+
details: { id: result.id, key: result.key }
|
|
20469
|
+
});
|
|
20470
|
+
return result;
|
|
20471
|
+
}
|
|
20472
|
+
/**
|
|
20473
|
+
* Delete a custom field definition (soft delete)
|
|
20474
|
+
*
|
|
20475
|
+
* Soft deletes a custom field definition. Existing user data in customData
|
|
20476
|
+
* is preserved, but the field will no longer appear in forms or validation.
|
|
20477
|
+
*
|
|
20478
|
+
* ⚠️ Consider the impact on existing user data before deleting.
|
|
20479
|
+
*
|
|
20480
|
+
* @param id - The UUID of the custom field definition to delete
|
|
20481
|
+
* @throws {PersApiError} When definition not found (404)
|
|
20482
|
+
*
|
|
20483
|
+
* @example
|
|
20484
|
+
* ```typescript
|
|
20485
|
+
* try {
|
|
20486
|
+
* await sdk.customFields.deleteDefinition('uuid-here');
|
|
20487
|
+
* console.log('Field deleted');
|
|
20488
|
+
* } catch (error) {
|
|
20489
|
+
* console.log('Failed to delete:', error.message);
|
|
20490
|
+
* }
|
|
20491
|
+
* ```
|
|
20492
|
+
*/
|
|
20493
|
+
async deleteDefinition(id) {
|
|
20494
|
+
await this.api.deleteDefinition(id);
|
|
20495
|
+
// Using 'user' domain until pers-shared adds 'customField' domain
|
|
20496
|
+
this.events?.emitSuccess({
|
|
20497
|
+
domain: 'user',
|
|
20498
|
+
type: 'definition_deleted',
|
|
20499
|
+
userMessage: 'Custom field deleted',
|
|
20500
|
+
details: { id }
|
|
20501
|
+
});
|
|
20502
|
+
}
|
|
20503
|
+
/**
|
|
20504
|
+
* Resolve dynamic select options for a field
|
|
20505
|
+
*
|
|
20506
|
+
* For fields with selectOptionsSource (dynamic options from entities),
|
|
20507
|
+
* this resolves the actual options by querying the source entity.
|
|
20508
|
+
*
|
|
20509
|
+
* @param id - The UUID of the custom field definition
|
|
20510
|
+
* @returns Array of select options
|
|
20511
|
+
*
|
|
20512
|
+
* @example
|
|
20513
|
+
* ```typescript
|
|
20514
|
+
* const field = await sdk.customFields.getDefinition('hotel-field-id');
|
|
20515
|
+
*
|
|
20516
|
+
* if (field.selectOptionsSource) {
|
|
20517
|
+
* // Options come from entity - need to resolve
|
|
20518
|
+
* const options = await sdk.customFields.resolveSelectOptions(field.id);
|
|
20519
|
+
* console.log('Available hotels:', options);
|
|
20520
|
+
* } else {
|
|
20521
|
+
* // Static options
|
|
20522
|
+
* console.log('Options:', field.selectOptions);
|
|
20523
|
+
* }
|
|
20524
|
+
* ```
|
|
20525
|
+
*/
|
|
20526
|
+
async resolveSelectOptions(id) {
|
|
20527
|
+
const response = await this.api.resolveSelectOptions(id);
|
|
20528
|
+
return response.options;
|
|
20529
|
+
}
|
|
20530
|
+
/**
|
|
20531
|
+
* Validate user custom data against field definitions
|
|
20532
|
+
*
|
|
20533
|
+
* Client-side validation using the same rules as the backend. Use this
|
|
20534
|
+
* to validate form data before submission.
|
|
20535
|
+
*
|
|
20536
|
+
* @param data - User's custom data (key-value pairs)
|
|
20537
|
+
* @param definitions - Custom field definitions to validate against
|
|
20538
|
+
* @returns Array of validation errors (empty if valid)
|
|
20539
|
+
*
|
|
20540
|
+
* @example Form Validation
|
|
20541
|
+
* ```typescript
|
|
20542
|
+
* const definitions = await sdk.customFields.getDefinitions();
|
|
20543
|
+
* const formData = {
|
|
20544
|
+
* employee_id: 'E123', // Missing a digit
|
|
20545
|
+
* department: 'engineering'
|
|
20546
|
+
* };
|
|
20547
|
+
*
|
|
20548
|
+
* const errors = sdk.customFields.validateUserData(formData, definitions);
|
|
20549
|
+
* if (errors.length > 0) {
|
|
20550
|
+
* errors.forEach(err => {
|
|
20551
|
+
* console.log(`${err.field}: ${err.message}`);
|
|
20552
|
+
* });
|
|
20553
|
+
* return; // Don't submit
|
|
20554
|
+
* }
|
|
20555
|
+
*
|
|
20556
|
+
* // No errors, safe to submit
|
|
20557
|
+
* await sdk.users.updateCurrentUser({ customData: formData });
|
|
20558
|
+
* ```
|
|
20559
|
+
*/
|
|
20560
|
+
validateUserData(data, definitions) {
|
|
20561
|
+
// Convert CustomFieldDefinitionDTO to FieldDefinition for validation
|
|
20562
|
+
const fieldDefinitions = definitions.map(def => ({
|
|
20563
|
+
key: def.key,
|
|
20564
|
+
fieldType: def.fieldType,
|
|
20565
|
+
label: def.label,
|
|
20566
|
+
validation: def.validation ?? undefined,
|
|
20567
|
+
}));
|
|
20568
|
+
return validateAllFields(data, fieldDefinitions);
|
|
20569
|
+
}
|
|
20570
|
+
/**
|
|
20571
|
+
* Validate a single field value
|
|
20572
|
+
*
|
|
20573
|
+
* Validates a single value against a field's rules. Useful for
|
|
20574
|
+
* real-time validation as the user types.
|
|
20575
|
+
*
|
|
20576
|
+
* @param value - The value to validate
|
|
20577
|
+
* @param definition - The field definition
|
|
20578
|
+
* @returns Validation error or null if valid
|
|
20579
|
+
*
|
|
20580
|
+
* @example Real-time Validation
|
|
20581
|
+
* ```typescript
|
|
20582
|
+
* const employeeIdField = definitions.find(d => d.key === 'employee_id');
|
|
20583
|
+
*
|
|
20584
|
+
* const handleBlur = (value: string) => {
|
|
20585
|
+
* const error = sdk.customFields.validateFieldValue(value, employeeIdField);
|
|
20586
|
+
* if (error) {
|
|
20587
|
+
* setFieldError('employee_id', error.message);
|
|
20588
|
+
* } else {
|
|
20589
|
+
* clearFieldError('employee_id');
|
|
20590
|
+
* }
|
|
20591
|
+
* };
|
|
20592
|
+
* ```
|
|
20593
|
+
*/
|
|
20594
|
+
validateFieldValue(value, definition) {
|
|
20595
|
+
const error = validateFieldValue(value, definition.validation ?? null, definition.fieldType);
|
|
20596
|
+
if (error) {
|
|
20597
|
+
// Add field key to error
|
|
20598
|
+
return { ...error, field: definition.key };
|
|
20599
|
+
}
|
|
20600
|
+
return null;
|
|
20601
|
+
}
|
|
20602
|
+
}
|
|
20603
|
+
|
|
19308
20604
|
/**
|
|
19309
20605
|
* @fileoverview PERS SDK - Platform-agnostic TypeScript SDK with High-Level Managers
|
|
19310
20606
|
*
|
|
@@ -19748,7 +21044,7 @@ class PersSDK {
|
|
|
19748
21044
|
*/
|
|
19749
21045
|
get tokens() {
|
|
19750
21046
|
if (!this._tokens) {
|
|
19751
|
-
this._tokens = new TokenManager(this.apiClient);
|
|
21047
|
+
this._tokens = new TokenManager(this.apiClient, this._events);
|
|
19752
21048
|
}
|
|
19753
21049
|
return this._tokens;
|
|
19754
21050
|
}
|
|
@@ -19993,6 +21289,37 @@ class PersSDK {
|
|
|
19993
21289
|
}
|
|
19994
21290
|
return this._webhooks;
|
|
19995
21291
|
}
|
|
21292
|
+
/**
|
|
21293
|
+
* Custom Field Definition Manager - Manage tenant-specific custom fields
|
|
21294
|
+
*
|
|
21295
|
+
* Provides CRUD operations for custom field definitions that extend the built-in
|
|
21296
|
+
* user profile fields. Custom fields are tenant-specific and support validation.
|
|
21297
|
+
*
|
|
21298
|
+
* @returns CustomFieldDefinitionManager instance
|
|
21299
|
+
*
|
|
21300
|
+
* @example Basic Operations
|
|
21301
|
+
* ```typescript
|
|
21302
|
+
* // List all custom fields
|
|
21303
|
+
* const fields = await sdk.customFields.getDefinitions();
|
|
21304
|
+
*
|
|
21305
|
+
* // Create a new field
|
|
21306
|
+
* const field = await sdk.customFields.createDefinition({
|
|
21307
|
+
* key: 'employee_id',
|
|
21308
|
+
* label: 'Employee ID',
|
|
21309
|
+
* fieldType: 'text',
|
|
21310
|
+
* validation: { required: true }
|
|
21311
|
+
* });
|
|
21312
|
+
*
|
|
21313
|
+
* // Validate user data
|
|
21314
|
+
* const errors = sdk.customFields.validateUserData(formData, fields.data);
|
|
21315
|
+
* ```
|
|
21316
|
+
*/
|
|
21317
|
+
get customFields() {
|
|
21318
|
+
if (!this._customFields) {
|
|
21319
|
+
this._customFields = new CustomFieldDefinitionManager(this.apiClient, this._events);
|
|
21320
|
+
}
|
|
21321
|
+
return this._customFields;
|
|
21322
|
+
}
|
|
19996
21323
|
/**
|
|
19997
21324
|
* Wallet Events Manager - Real-time blockchain events for user's wallets
|
|
19998
21325
|
*
|
|
@@ -40680,6 +42007,122 @@ function spelunkMessage(value) {
|
|
|
40680
42007
|
return result;
|
|
40681
42008
|
}
|
|
40682
42009
|
|
|
42010
|
+
/**
|
|
42011
|
+
* Get multiple ERC-1155 token balances using native balanceOfBatch function.
|
|
42012
|
+
* This is the most efficient method - it's built into the ERC-1155 standard.
|
|
42013
|
+
*
|
|
42014
|
+
* @param contract - ERC-1155 contract instance
|
|
42015
|
+
* @param accountAddress - Account to check balances for
|
|
42016
|
+
* @param tokenIds - Array of tokenIds to check
|
|
42017
|
+
* @returns Array of BatchBalanceResult with balance for each tokenId
|
|
42018
|
+
*
|
|
42019
|
+
* @example
|
|
42020
|
+
* ```typescript
|
|
42021
|
+
* const results = await getERC1155BalanceOfBatch(
|
|
42022
|
+
* contract,
|
|
42023
|
+
* '0xabc...',
|
|
42024
|
+
* ['1', '2', '3', '4', '5']
|
|
42025
|
+
* );
|
|
42026
|
+
* // 1 RPC call returns all balances
|
|
42027
|
+
* ```
|
|
42028
|
+
*/
|
|
42029
|
+
async function getERC1155BalanceOfBatch(contract, accountAddress, tokenIds) {
|
|
42030
|
+
if (tokenIds.length === 0) {
|
|
42031
|
+
return [];
|
|
42032
|
+
}
|
|
42033
|
+
try {
|
|
42034
|
+
// ERC-1155 balanceOfBatch requires arrays of same length
|
|
42035
|
+
// accounts[i] corresponds to ids[i]
|
|
42036
|
+
const accounts = tokenIds.map(() => accountAddress);
|
|
42037
|
+
// Call native balanceOfBatch - 1 RPC call for ALL tokenIds
|
|
42038
|
+
const balances = await contract.balanceOfBatch(accounts, tokenIds);
|
|
42039
|
+
return tokenIds.map((tokenId, index) => ({
|
|
42040
|
+
tokenId,
|
|
42041
|
+
balance: balances[index],
|
|
42042
|
+
success: true
|
|
42043
|
+
}));
|
|
42044
|
+
}
|
|
42045
|
+
catch (error) {
|
|
42046
|
+
console.warn('[ERC1155] balanceOfBatch failed:', error);
|
|
42047
|
+
return tokenIds.map(tokenId => ({
|
|
42048
|
+
tokenId,
|
|
42049
|
+
balance: 0n,
|
|
42050
|
+
success: false,
|
|
42051
|
+
error: `balanceOfBatch failed: ${error}`
|
|
42052
|
+
}));
|
|
42053
|
+
}
|
|
42054
|
+
}
|
|
42055
|
+
/**
|
|
42056
|
+
* Get multiple token balances for ERC-1155 contracts.
|
|
42057
|
+
* Uses the native balanceOfBatch function (1 RPC call for ALL tokens).
|
|
42058
|
+
* Falls back to individual calls only if balanceOfBatch fails.
|
|
42059
|
+
*
|
|
42060
|
+
* @param provider - Ethers provider
|
|
42061
|
+
* @param contractAddress - Token contract address
|
|
42062
|
+
* @param contractInterface - Contract interface (must include balanceOfBatch for ERC-1155)
|
|
42063
|
+
* @param accountAddress - Account to check balances for
|
|
42064
|
+
* @param tokenIds - Array of tokenIds to check
|
|
42065
|
+
* @param isERC1155 - Whether the contract is ERC-1155 (default: true)
|
|
42066
|
+
* @returns Array of BatchBalanceResult
|
|
42067
|
+
*/
|
|
42068
|
+
async function getBatchBalancesWithFallback(provider, contractAddress, contractInterface, accountAddress, tokenIds, isERC1155 = true) {
|
|
42069
|
+
if (tokenIds.length === 0)
|
|
42070
|
+
return [];
|
|
42071
|
+
const contract = new Contract(contractAddress, contractInterface, provider);
|
|
42072
|
+
// For single token, direct call is more efficient
|
|
42073
|
+
if (tokenIds.length === 1) {
|
|
42074
|
+
try {
|
|
42075
|
+
const balance = isERC1155
|
|
42076
|
+
? await contract.balanceOf(accountAddress, tokenIds[0])
|
|
42077
|
+
: await contract.balanceOf(accountAddress);
|
|
42078
|
+
return [{
|
|
42079
|
+
tokenId: tokenIds[0],
|
|
42080
|
+
balance: balance,
|
|
42081
|
+
success: true
|
|
42082
|
+
}];
|
|
42083
|
+
}
|
|
42084
|
+
catch (error) {
|
|
42085
|
+
return [{
|
|
42086
|
+
tokenId: tokenIds[0],
|
|
42087
|
+
balance: 0n,
|
|
42088
|
+
success: false,
|
|
42089
|
+
error: `${error}`
|
|
42090
|
+
}];
|
|
42091
|
+
}
|
|
42092
|
+
}
|
|
42093
|
+
// For ERC-1155: Use native balanceOfBatch (1 RPC call for ALL tokens)
|
|
42094
|
+
if (isERC1155) {
|
|
42095
|
+
const results = await getERC1155BalanceOfBatch(contract, accountAddress, tokenIds);
|
|
42096
|
+
// If native call succeeded, return results
|
|
42097
|
+
if (results.some(r => r.success)) {
|
|
42098
|
+
return results;
|
|
42099
|
+
}
|
|
42100
|
+
// Otherwise fall through to individual calls
|
|
42101
|
+
}
|
|
42102
|
+
// Fallback: individual calls in parallel (for non-ERC1155 or if balanceOfBatch failed)
|
|
42103
|
+
const promises = tokenIds.map(async (tokenId) => {
|
|
42104
|
+
try {
|
|
42105
|
+
const balance = isERC1155
|
|
42106
|
+
? await contract.balanceOf(accountAddress, tokenId)
|
|
42107
|
+
: await contract.balanceOf(accountAddress);
|
|
42108
|
+
return {
|
|
42109
|
+
tokenId,
|
|
42110
|
+
balance: balance,
|
|
42111
|
+
success: true
|
|
42112
|
+
};
|
|
42113
|
+
}
|
|
42114
|
+
catch (error) {
|
|
42115
|
+
return {
|
|
42116
|
+
tokenId,
|
|
42117
|
+
balance: 0n,
|
|
42118
|
+
success: false,
|
|
42119
|
+
error: `${error}`
|
|
42120
|
+
};
|
|
42121
|
+
}
|
|
42122
|
+
});
|
|
42123
|
+
return Promise.all(promises);
|
|
42124
|
+
}
|
|
42125
|
+
|
|
40683
42126
|
const PERS_STATUS_TOKEN_ETHERS_METHOD_BUILDER = (contract, methodName) => {
|
|
40684
42127
|
try {
|
|
40685
42128
|
// Use unknown first, then cast to the expected type
|
|
@@ -41291,7 +42734,10 @@ class TokenDomainService {
|
|
|
41291
42734
|
try {
|
|
41292
42735
|
const abi = convertAbiToInterface(params.abi);
|
|
41293
42736
|
const analysis = this.contractService.analyzeContract(abi);
|
|
41294
|
-
|
|
42737
|
+
// ERC-1155 balanceOfBatch can handle large batches efficiently (1 RPC call)
|
|
42738
|
+
// Default to 100 for ERC-1155, 20 for ERC-721 (which has no batch function)
|
|
42739
|
+
const defaultBatchSize = analysis.isERC1155 ? 100 : 20;
|
|
42740
|
+
const batchSize = Math.min(params.batchSize || defaultBatchSize, analysis.isERC1155 ? 200 : 50);
|
|
41295
42741
|
// ERC-1155: Requires specific token IDs
|
|
41296
42742
|
if (analysis.isERC1155) {
|
|
41297
42743
|
if (!params.tokenIds?.length) {
|
|
@@ -41345,31 +42791,44 @@ class TokenDomainService {
|
|
|
41345
42791
|
async processTokenBatch(params, tokenIds, batchSize, tokenStandard) {
|
|
41346
42792
|
const tokens = [];
|
|
41347
42793
|
const errors = [];
|
|
42794
|
+
const abi = convertAbiToInterface(params.abi);
|
|
41348
42795
|
// Process in batches
|
|
41349
42796
|
for (let i = 0; i < tokenIds.length; i += batchSize) {
|
|
41350
42797
|
const batch = tokenIds.slice(i, i + batchSize);
|
|
41351
|
-
|
|
41352
|
-
|
|
41353
|
-
|
|
41354
|
-
|
|
41355
|
-
|
|
41356
|
-
|
|
41357
|
-
|
|
41358
|
-
|
|
41359
|
-
|
|
41360
|
-
|
|
41361
|
-
|
|
41362
|
-
|
|
41363
|
-
|
|
41364
|
-
|
|
41365
|
-
|
|
41366
|
-
|
|
41367
|
-
|
|
41368
|
-
|
|
41369
|
-
|
|
41370
|
-
|
|
41371
|
-
|
|
41372
|
-
|
|
42798
|
+
let validTokens;
|
|
42799
|
+
if (tokenStandard === 'ERC-721') {
|
|
42800
|
+
// For ERC-721: Skip balance check, we know ownership from enumeration or explicit tokenIds
|
|
42801
|
+
validTokens = batch.map(tokenId => ({
|
|
42802
|
+
tokenId,
|
|
42803
|
+
balance: 1,
|
|
42804
|
+
hasBalance: true,
|
|
42805
|
+
metadata: null
|
|
42806
|
+
}));
|
|
42807
|
+
}
|
|
42808
|
+
else {
|
|
42809
|
+
// For ERC-1155: Use native balanceOfBatch (1 RPC call for entire batch)
|
|
42810
|
+
const batchResults = await this.web3Api.getBatchTokenBalances({
|
|
42811
|
+
accountAddress: params.accountAddress,
|
|
42812
|
+
contractAddress: params.contractAddress,
|
|
42813
|
+
abi,
|
|
42814
|
+
tokenIds: batch,
|
|
42815
|
+
chainId: params.chainId,
|
|
42816
|
+
isERC1155: true
|
|
42817
|
+
});
|
|
42818
|
+
// Convert batch results to TokenBalance format
|
|
42819
|
+
validTokens = batchResults
|
|
42820
|
+
.filter(r => r.success && r.balance > 0n)
|
|
42821
|
+
.map(r => ({
|
|
42822
|
+
tokenId: r.tokenId,
|
|
42823
|
+
balance: Number(r.balance),
|
|
42824
|
+
hasBalance: true,
|
|
42825
|
+
metadata: null
|
|
42826
|
+
}));
|
|
42827
|
+
// Track errors
|
|
42828
|
+
batchResults.filter(r => !r.success).forEach(r => {
|
|
42829
|
+
errors.push(`${r.tokenId}: ${r.error}`);
|
|
42830
|
+
});
|
|
42831
|
+
}
|
|
41373
42832
|
// Step 2: Get metadata for tokens that have balance
|
|
41374
42833
|
if (validTokens.length > 0) {
|
|
41375
42834
|
const metadataPromises = validTokens.map(token => this.getTokenMetadata({
|
|
@@ -41493,6 +42952,627 @@ class ContractDomainService {
|
|
|
41493
42952
|
}
|
|
41494
42953
|
}
|
|
41495
42954
|
|
|
42955
|
+
/**
|
|
42956
|
+
* TokenCollectionManager - Auto-refresh token collections on wallet events
|
|
42957
|
+
*
|
|
42958
|
+
* This manager provides:
|
|
42959
|
+
* 1. Cached token collection storage with automatic refresh
|
|
42960
|
+
* 2. Auto-subscription to wallet events (transfers, mints, burns)
|
|
42961
|
+
* 3. Targeted invalidation based on contract address
|
|
42962
|
+
* 4. Subscriber pattern for reactive updates
|
|
42963
|
+
*
|
|
42964
|
+
* @example
|
|
42965
|
+
* ```typescript
|
|
42966
|
+
* // Get from SDK
|
|
42967
|
+
* const manager = sdk.tokens.collectionManager;
|
|
42968
|
+
*
|
|
42969
|
+
* // Subscribe to collection updates
|
|
42970
|
+
* const unsubscribe = manager.subscribe((collection, contractAddress) => {
|
|
42971
|
+
* console.log(`Collection updated for ${contractAddress}`);
|
|
42972
|
+
* updateUI(collection);
|
|
42973
|
+
* });
|
|
42974
|
+
*
|
|
42975
|
+
* // Fetch collection (auto-cached, auto-refreshed on wallet events)
|
|
42976
|
+
* const collection = await manager.getCollection({
|
|
42977
|
+
* accountAddress: '0x...',
|
|
42978
|
+
* contractAddress: '0x...',
|
|
42979
|
+
* abi: contractAbi,
|
|
42980
|
+
* chainId: 39123,
|
|
42981
|
+
* tokenIds: ['1', '2', '3']
|
|
42982
|
+
* });
|
|
42983
|
+
*
|
|
42984
|
+
* // Cleanup
|
|
42985
|
+
* unsubscribe();
|
|
42986
|
+
* ```
|
|
42987
|
+
*/
|
|
42988
|
+
/**
|
|
42989
|
+
* Manages token collections with automatic refresh on wallet events
|
|
42990
|
+
*/
|
|
42991
|
+
class TokenCollectionManager {
|
|
42992
|
+
constructor(tokenService, eventEmitter, config) {
|
|
42993
|
+
this.tokenService = tokenService;
|
|
42994
|
+
this.eventEmitter = eventEmitter;
|
|
42995
|
+
this.collections = new Map();
|
|
42996
|
+
this.handlers = new Set();
|
|
42997
|
+
this.pendingRefresh = new Map();
|
|
42998
|
+
this.config = {
|
|
42999
|
+
autoRefreshOnWalletEvents: config?.autoRefreshOnWalletEvents ?? true,
|
|
43000
|
+
eventDebounceMs: config?.eventDebounceMs ?? 500,
|
|
43001
|
+
debug: config?.debug ?? false
|
|
43002
|
+
};
|
|
43003
|
+
if (this.config.autoRefreshOnWalletEvents && eventEmitter) {
|
|
43004
|
+
this.setupEventSubscription();
|
|
43005
|
+
}
|
|
43006
|
+
}
|
|
43007
|
+
/**
|
|
43008
|
+
* Subscribe to wallet events for auto-refresh
|
|
43009
|
+
*/
|
|
43010
|
+
setupEventSubscription() {
|
|
43011
|
+
if (!this.eventEmitter) {
|
|
43012
|
+
return;
|
|
43013
|
+
}
|
|
43014
|
+
this.eventUnsubscribe = this.eventEmitter.subscribe((event) => {
|
|
43015
|
+
const contractAddress = event.details?.contractAddress;
|
|
43016
|
+
this.handleWalletEvent(contractAddress, event.type);
|
|
43017
|
+
}, { domains: ['wallet'] });
|
|
43018
|
+
}
|
|
43019
|
+
/**
|
|
43020
|
+
* Get a token collection (cached, auto-refreshed on wallet events)
|
|
43021
|
+
*
|
|
43022
|
+
* @param params - Collection parameters
|
|
43023
|
+
* @returns Promise resolving to the token collection
|
|
43024
|
+
*/
|
|
43025
|
+
async getCollection(params) {
|
|
43026
|
+
const key = this.getCacheKey(params);
|
|
43027
|
+
const cached = this.collections.get(key);
|
|
43028
|
+
// Return cached if available and not stale
|
|
43029
|
+
if (cached?.lastCollection) {
|
|
43030
|
+
this.log(`Cache HIT for ${key}`);
|
|
43031
|
+
return cached.lastCollection;
|
|
43032
|
+
}
|
|
43033
|
+
this.log(`Cache MISS for ${key}, fetching...`);
|
|
43034
|
+
return this.fetchAndCache(params);
|
|
43035
|
+
}
|
|
43036
|
+
/**
|
|
43037
|
+
* Force refresh a specific collection
|
|
43038
|
+
*
|
|
43039
|
+
* @param contractAddress - Contract address to refresh
|
|
43040
|
+
* @param accountAddress - Account address to refresh
|
|
43041
|
+
* @returns Promise that resolves when refresh is complete
|
|
43042
|
+
*/
|
|
43043
|
+
async refreshCollection(contractAddress, accountAddress) {
|
|
43044
|
+
const toRefresh = [];
|
|
43045
|
+
for (const [key, entry] of this.collections) {
|
|
43046
|
+
if (entry.params.contractAddress === contractAddress) {
|
|
43047
|
+
if (!accountAddress || entry.params.accountAddress === accountAddress) {
|
|
43048
|
+
toRefresh.push(key);
|
|
43049
|
+
}
|
|
43050
|
+
}
|
|
43051
|
+
}
|
|
43052
|
+
await Promise.all(toRefresh.map(key => {
|
|
43053
|
+
const entry = this.collections.get(key);
|
|
43054
|
+
if (entry) {
|
|
43055
|
+
return this.refreshEntry(key, entry, 'refresh');
|
|
43056
|
+
}
|
|
43057
|
+
}));
|
|
43058
|
+
}
|
|
43059
|
+
/**
|
|
43060
|
+
* Subscribe to collection change events
|
|
43061
|
+
*
|
|
43062
|
+
* @param handler - Callback when any tracked collection changes
|
|
43063
|
+
* @returns Unsubscribe function
|
|
43064
|
+
*/
|
|
43065
|
+
subscribe(handler) {
|
|
43066
|
+
this.handlers.add(handler);
|
|
43067
|
+
return () => this.handlers.delete(handler);
|
|
43068
|
+
}
|
|
43069
|
+
/**
|
|
43070
|
+
* Get all currently tracked collections
|
|
43071
|
+
*/
|
|
43072
|
+
getTrackedCollections() {
|
|
43073
|
+
return Array.from(this.collections.entries()).map(([_, entry]) => ({
|
|
43074
|
+
contractAddress: entry.params.contractAddress,
|
|
43075
|
+
accountAddress: entry.params.accountAddress,
|
|
43076
|
+
collection: entry.lastCollection
|
|
43077
|
+
}));
|
|
43078
|
+
}
|
|
43079
|
+
/**
|
|
43080
|
+
* Check if a collection is currently being tracked
|
|
43081
|
+
*/
|
|
43082
|
+
isTracking(contractAddress, accountAddress) {
|
|
43083
|
+
const key = `${contractAddress}:${accountAddress}`;
|
|
43084
|
+
return this.collections.has(key);
|
|
43085
|
+
}
|
|
43086
|
+
/**
|
|
43087
|
+
* Clear tracking for a specific collection
|
|
43088
|
+
*/
|
|
43089
|
+
untrack(contractAddress, accountAddress) {
|
|
43090
|
+
if (accountAddress) {
|
|
43091
|
+
const key = `${contractAddress}:${accountAddress}`;
|
|
43092
|
+
this.collections.delete(key);
|
|
43093
|
+
this.log(`Untracked ${key}`);
|
|
43094
|
+
}
|
|
43095
|
+
else {
|
|
43096
|
+
// Untrack all for this contract
|
|
43097
|
+
for (const key of this.collections.keys()) {
|
|
43098
|
+
if (key.startsWith(`${contractAddress}:`)) {
|
|
43099
|
+
this.collections.delete(key);
|
|
43100
|
+
}
|
|
43101
|
+
}
|
|
43102
|
+
this.log(`Untracked all collections for ${contractAddress}`);
|
|
43103
|
+
}
|
|
43104
|
+
}
|
|
43105
|
+
/**
|
|
43106
|
+
* Clear all tracked collections
|
|
43107
|
+
*/
|
|
43108
|
+
clearAll() {
|
|
43109
|
+
this.collections.clear();
|
|
43110
|
+
this.log('Cleared all tracked collections');
|
|
43111
|
+
}
|
|
43112
|
+
/**
|
|
43113
|
+
* Handle wallet events (with debouncing)
|
|
43114
|
+
*/
|
|
43115
|
+
handleWalletEvent(contractAddress, eventType) {
|
|
43116
|
+
// Determine which collections to refresh
|
|
43117
|
+
const keysToRefresh = [];
|
|
43118
|
+
for (const [key, entry] of this.collections) {
|
|
43119
|
+
// If no specific contract, refresh all; otherwise match contract
|
|
43120
|
+
if (!contractAddress || entry.params.contractAddress.toLowerCase() === contractAddress.toLowerCase()) {
|
|
43121
|
+
keysToRefresh.push(key);
|
|
43122
|
+
}
|
|
43123
|
+
}
|
|
43124
|
+
if (keysToRefresh.length === 0) {
|
|
43125
|
+
this.log(`No tracked collections affected by event for ${contractAddress || 'all contracts'}`);
|
|
43126
|
+
return;
|
|
43127
|
+
}
|
|
43128
|
+
// Debounce refresh per key
|
|
43129
|
+
for (const key of keysToRefresh) {
|
|
43130
|
+
const existing = this.pendingRefresh.get(key);
|
|
43131
|
+
if (existing) {
|
|
43132
|
+
clearTimeout(existing);
|
|
43133
|
+
}
|
|
43134
|
+
const timeout = setTimeout(async () => {
|
|
43135
|
+
this.pendingRefresh.delete(key);
|
|
43136
|
+
const entry = this.collections.get(key);
|
|
43137
|
+
if (entry) {
|
|
43138
|
+
this.log(`Refreshing ${key} due to wallet event: ${eventType}`);
|
|
43139
|
+
await this.refreshEntry(key, entry, eventType);
|
|
43140
|
+
}
|
|
43141
|
+
}, this.config.eventDebounceMs);
|
|
43142
|
+
this.pendingRefresh.set(key, timeout);
|
|
43143
|
+
}
|
|
43144
|
+
}
|
|
43145
|
+
/**
|
|
43146
|
+
* Fetch and cache a collection
|
|
43147
|
+
*/
|
|
43148
|
+
async fetchAndCache(params) {
|
|
43149
|
+
const key = this.getCacheKey(params);
|
|
43150
|
+
// Mark as refreshing to prevent duplicate fetches
|
|
43151
|
+
const existing = this.collections.get(key);
|
|
43152
|
+
if (existing?.refreshing) {
|
|
43153
|
+
// Wait for existing refresh to complete
|
|
43154
|
+
return existing.lastCollection || this.emptyCollection(params);
|
|
43155
|
+
}
|
|
43156
|
+
this.collections.set(key, {
|
|
43157
|
+
params,
|
|
43158
|
+
lastCollection: null,
|
|
43159
|
+
timestamp: Date.now(),
|
|
43160
|
+
refreshing: true
|
|
43161
|
+
});
|
|
43162
|
+
try {
|
|
43163
|
+
const collection = await this.tokenService.getTokenCollection(params);
|
|
43164
|
+
this.collections.set(key, {
|
|
43165
|
+
params,
|
|
43166
|
+
lastCollection: collection,
|
|
43167
|
+
timestamp: Date.now(),
|
|
43168
|
+
refreshing: false
|
|
43169
|
+
});
|
|
43170
|
+
this.log(`Cached collection for ${key}: ${collection.tokensRetrieved} tokens`);
|
|
43171
|
+
return collection;
|
|
43172
|
+
}
|
|
43173
|
+
catch (error) {
|
|
43174
|
+
// Remove failed entry
|
|
43175
|
+
this.collections.delete(key);
|
|
43176
|
+
throw error;
|
|
43177
|
+
}
|
|
43178
|
+
}
|
|
43179
|
+
/**
|
|
43180
|
+
* Refresh a cached entry and notify handlers
|
|
43181
|
+
*/
|
|
43182
|
+
async refreshEntry(key, entry, source) {
|
|
43183
|
+
if (entry.refreshing) {
|
|
43184
|
+
this.log(`Skipping refresh for ${key} - already refreshing`);
|
|
43185
|
+
return;
|
|
43186
|
+
}
|
|
43187
|
+
entry.refreshing = true;
|
|
43188
|
+
try {
|
|
43189
|
+
const newCollection = await this.tokenService.getTokenCollection(entry.params);
|
|
43190
|
+
entry.lastCollection = newCollection;
|
|
43191
|
+
entry.timestamp = Date.now();
|
|
43192
|
+
entry.refreshing = false;
|
|
43193
|
+
this.log(`Refreshed ${key}: ${newCollection.tokensRetrieved} tokens`);
|
|
43194
|
+
// Notify subscribers
|
|
43195
|
+
for (const handler of this.handlers) {
|
|
43196
|
+
try {
|
|
43197
|
+
handler(newCollection, entry.params.contractAddress, {
|
|
43198
|
+
type: source === 'refresh' ? 'refresh' : 'wallet_event',
|
|
43199
|
+
source
|
|
43200
|
+
});
|
|
43201
|
+
}
|
|
43202
|
+
catch (handlerError) {
|
|
43203
|
+
console.error('[TokenCollectionManager] Handler error:', handlerError);
|
|
43204
|
+
}
|
|
43205
|
+
}
|
|
43206
|
+
}
|
|
43207
|
+
catch (error) {
|
|
43208
|
+
entry.refreshing = false;
|
|
43209
|
+
console.error(`[TokenCollectionManager] Refresh failed for ${key}:`, error);
|
|
43210
|
+
}
|
|
43211
|
+
}
|
|
43212
|
+
/**
|
|
43213
|
+
* Generate cache key from params
|
|
43214
|
+
*/
|
|
43215
|
+
getCacheKey(params) {
|
|
43216
|
+
return `${params.contractAddress}:${params.accountAddress}`;
|
|
43217
|
+
}
|
|
43218
|
+
/**
|
|
43219
|
+
* Create empty collection placeholder
|
|
43220
|
+
*/
|
|
43221
|
+
emptyCollection(params) {
|
|
43222
|
+
return {
|
|
43223
|
+
accountAddress: params.accountAddress,
|
|
43224
|
+
contractAddress: params.contractAddress,
|
|
43225
|
+
totalBalance: 0,
|
|
43226
|
+
tokensRetrieved: 0,
|
|
43227
|
+
tokens: [],
|
|
43228
|
+
note: 'Loading...'
|
|
43229
|
+
};
|
|
43230
|
+
}
|
|
43231
|
+
/**
|
|
43232
|
+
* Debug logging
|
|
43233
|
+
*/
|
|
43234
|
+
log(message, data) {
|
|
43235
|
+
if (this.config.debug) {
|
|
43236
|
+
console.log(`[TokenCollectionManager] ${message}`, data || '');
|
|
43237
|
+
}
|
|
43238
|
+
}
|
|
43239
|
+
/**
|
|
43240
|
+
* Cleanup resources
|
|
43241
|
+
*/
|
|
43242
|
+
destroy() {
|
|
43243
|
+
// Clear pending refreshes
|
|
43244
|
+
for (const timeout of this.pendingRefresh.values()) {
|
|
43245
|
+
clearTimeout(timeout);
|
|
43246
|
+
}
|
|
43247
|
+
this.pendingRefresh.clear();
|
|
43248
|
+
// Unsubscribe from events
|
|
43249
|
+
this.eventUnsubscribe?.();
|
|
43250
|
+
// Clear handlers and collections
|
|
43251
|
+
this.handlers.clear();
|
|
43252
|
+
this.collections.clear();
|
|
43253
|
+
this.log('Destroyed');
|
|
43254
|
+
}
|
|
43255
|
+
}
|
|
43256
|
+
|
|
43257
|
+
/**
|
|
43258
|
+
* BalanceManager - Auto-refresh ERC-20 token balances on wallet events
|
|
43259
|
+
*
|
|
43260
|
+
* This manager provides:
|
|
43261
|
+
* 1. Cached balance storage with automatic refresh
|
|
43262
|
+
* 2. Auto-subscription to wallet events (transfers)
|
|
43263
|
+
* 3. Targeted invalidation based on contract address
|
|
43264
|
+
* 4. Subscriber pattern for reactive updates
|
|
43265
|
+
*
|
|
43266
|
+
* @example
|
|
43267
|
+
* ```typescript
|
|
43268
|
+
* // Get from SDK
|
|
43269
|
+
* const manager = sdk.tokens.balanceManager;
|
|
43270
|
+
*
|
|
43271
|
+
* // Subscribe to balance updates
|
|
43272
|
+
* const unsubscribe = manager.subscribe((balance, contractAddress) => {
|
|
43273
|
+
* console.log(`Balance updated for ${contractAddress}: ${balance}`);
|
|
43274
|
+
* updateUI(balance);
|
|
43275
|
+
* });
|
|
43276
|
+
*
|
|
43277
|
+
* // Fetch balance (auto-cached, auto-refreshed on wallet events)
|
|
43278
|
+
* const balance = await manager.getBalance({
|
|
43279
|
+
* accountAddress: '0x...',
|
|
43280
|
+
* contractAddress: '0x...',
|
|
43281
|
+
* abi: contractAbi,
|
|
43282
|
+
* chainId: 39123
|
|
43283
|
+
* });
|
|
43284
|
+
*
|
|
43285
|
+
* // Manual refresh
|
|
43286
|
+
* await manager.refreshBalance(contractAddress);
|
|
43287
|
+
*
|
|
43288
|
+
* // Cleanup
|
|
43289
|
+
* unsubscribe();
|
|
43290
|
+
* ```
|
|
43291
|
+
*/
|
|
43292
|
+
/**
|
|
43293
|
+
* Manages ERC-20 token balances with automatic refresh on wallet events
|
|
43294
|
+
*/
|
|
43295
|
+
class BalanceManager {
|
|
43296
|
+
constructor(tokenService, eventEmitter, config) {
|
|
43297
|
+
this.tokenService = tokenService;
|
|
43298
|
+
this.eventEmitter = eventEmitter;
|
|
43299
|
+
this.balances = new Map();
|
|
43300
|
+
this.handlers = new Set();
|
|
43301
|
+
this.pendingRefresh = new Map();
|
|
43302
|
+
this.config = {
|
|
43303
|
+
autoRefreshOnWalletEvents: config?.autoRefreshOnWalletEvents ?? true,
|
|
43304
|
+
eventDebounceMs: config?.eventDebounceMs ?? 500,
|
|
43305
|
+
debug: config?.debug ?? false
|
|
43306
|
+
};
|
|
43307
|
+
if (this.config.autoRefreshOnWalletEvents && eventEmitter) {
|
|
43308
|
+
this.setupEventSubscription();
|
|
43309
|
+
}
|
|
43310
|
+
}
|
|
43311
|
+
/**
|
|
43312
|
+
* Subscribe to wallet events for auto-refresh
|
|
43313
|
+
*/
|
|
43314
|
+
setupEventSubscription() {
|
|
43315
|
+
if (!this.eventEmitter) {
|
|
43316
|
+
return;
|
|
43317
|
+
}
|
|
43318
|
+
this.eventUnsubscribe = this.eventEmitter.subscribe((event) => {
|
|
43319
|
+
const contractAddress = event.details?.contractAddress;
|
|
43320
|
+
this.handleWalletEvent(contractAddress, event.type);
|
|
43321
|
+
}, { domains: ['wallet'] });
|
|
43322
|
+
}
|
|
43323
|
+
/**
|
|
43324
|
+
* Get a token balance (cached, auto-refreshed on wallet events)
|
|
43325
|
+
*
|
|
43326
|
+
* @param params - Balance parameters
|
|
43327
|
+
* @returns Promise resolving to the token balance
|
|
43328
|
+
*/
|
|
43329
|
+
async getBalance(params) {
|
|
43330
|
+
const key = this.getCacheKey(params);
|
|
43331
|
+
const cached = this.balances.get(key);
|
|
43332
|
+
// Return cached if available and not stale
|
|
43333
|
+
if (cached?.lastBalance) {
|
|
43334
|
+
this.log(`Cache HIT for ${key}`);
|
|
43335
|
+
return cached.lastBalance;
|
|
43336
|
+
}
|
|
43337
|
+
this.log(`Cache MISS for ${key}, fetching...`);
|
|
43338
|
+
return this.fetchAndCache(params);
|
|
43339
|
+
}
|
|
43340
|
+
/**
|
|
43341
|
+
* Force refresh a specific balance
|
|
43342
|
+
*
|
|
43343
|
+
* @param contractAddress - Contract address to refresh
|
|
43344
|
+
* @param accountAddress - Account address to refresh (optional, refreshes all if not provided)
|
|
43345
|
+
* @returns Promise that resolves when refresh is complete
|
|
43346
|
+
*/
|
|
43347
|
+
async refreshBalance(contractAddress, accountAddress) {
|
|
43348
|
+
const toRefresh = [];
|
|
43349
|
+
for (const [key, entry] of this.balances) {
|
|
43350
|
+
if (entry.params.contractAddress.toLowerCase() === contractAddress.toLowerCase()) {
|
|
43351
|
+
if (!accountAddress || entry.params.accountAddress.toLowerCase() === accountAddress.toLowerCase()) {
|
|
43352
|
+
toRefresh.push(key);
|
|
43353
|
+
}
|
|
43354
|
+
}
|
|
43355
|
+
}
|
|
43356
|
+
await Promise.all(toRefresh.map(key => {
|
|
43357
|
+
const entry = this.balances.get(key);
|
|
43358
|
+
if (entry) {
|
|
43359
|
+
return this.refreshEntry(key, entry, 'refresh');
|
|
43360
|
+
}
|
|
43361
|
+
}));
|
|
43362
|
+
}
|
|
43363
|
+
/**
|
|
43364
|
+
* Force refresh all tracked balances
|
|
43365
|
+
*
|
|
43366
|
+
* @returns Promise that resolves when all refreshes are complete
|
|
43367
|
+
*/
|
|
43368
|
+
async refreshAll() {
|
|
43369
|
+
const refreshPromises = Array.from(this.balances.entries()).map(([key, entry]) => this.refreshEntry(key, entry, 'refresh'));
|
|
43370
|
+
await Promise.all(refreshPromises);
|
|
43371
|
+
}
|
|
43372
|
+
/**
|
|
43373
|
+
* Subscribe to balance change events
|
|
43374
|
+
*
|
|
43375
|
+
* @param handler - Callback when any tracked balance changes
|
|
43376
|
+
* @returns Unsubscribe function
|
|
43377
|
+
*/
|
|
43378
|
+
subscribe(handler) {
|
|
43379
|
+
this.handlers.add(handler);
|
|
43380
|
+
return () => this.handlers.delete(handler);
|
|
43381
|
+
}
|
|
43382
|
+
/**
|
|
43383
|
+
* Get all currently tracked balances
|
|
43384
|
+
*/
|
|
43385
|
+
getTrackedBalances() {
|
|
43386
|
+
return Array.from(this.balances.entries()).map(([_, entry]) => ({
|
|
43387
|
+
contractAddress: entry.params.contractAddress,
|
|
43388
|
+
accountAddress: entry.params.accountAddress,
|
|
43389
|
+
balance: entry.lastBalance
|
|
43390
|
+
}));
|
|
43391
|
+
}
|
|
43392
|
+
/**
|
|
43393
|
+
* Check if a balance is currently being tracked
|
|
43394
|
+
*/
|
|
43395
|
+
isTracking(contractAddress, accountAddress) {
|
|
43396
|
+
const key = `${contractAddress.toLowerCase()}:${accountAddress.toLowerCase()}`;
|
|
43397
|
+
return this.balances.has(key);
|
|
43398
|
+
}
|
|
43399
|
+
/**
|
|
43400
|
+
* Clear tracking for a specific balance
|
|
43401
|
+
*/
|
|
43402
|
+
untrack(contractAddress, accountAddress) {
|
|
43403
|
+
if (accountAddress) {
|
|
43404
|
+
const key = `${contractAddress.toLowerCase()}:${accountAddress.toLowerCase()}`;
|
|
43405
|
+
this.balances.delete(key);
|
|
43406
|
+
this.log(`Untracked ${key}`);
|
|
43407
|
+
}
|
|
43408
|
+
else {
|
|
43409
|
+
// Untrack all for this contract
|
|
43410
|
+
const toDelete = [];
|
|
43411
|
+
for (const key of this.balances.keys()) {
|
|
43412
|
+
if (key.startsWith(`${contractAddress.toLowerCase()}:`)) {
|
|
43413
|
+
toDelete.push(key);
|
|
43414
|
+
}
|
|
43415
|
+
}
|
|
43416
|
+
toDelete.forEach(key => this.balances.delete(key));
|
|
43417
|
+
this.log(`Untracked all balances for ${contractAddress}`);
|
|
43418
|
+
}
|
|
43419
|
+
}
|
|
43420
|
+
/**
|
|
43421
|
+
* Clear all tracked balances
|
|
43422
|
+
*/
|
|
43423
|
+
clearAll() {
|
|
43424
|
+
this.balances.clear();
|
|
43425
|
+
this.log('Cleared all tracked balances');
|
|
43426
|
+
}
|
|
43427
|
+
/**
|
|
43428
|
+
* Handle wallet events (with debouncing)
|
|
43429
|
+
*/
|
|
43430
|
+
handleWalletEvent(contractAddress, eventType) {
|
|
43431
|
+
// Determine which balances to refresh
|
|
43432
|
+
const keysToRefresh = [];
|
|
43433
|
+
for (const [key, entry] of this.balances) {
|
|
43434
|
+
// If no specific contract, refresh all; otherwise match contract
|
|
43435
|
+
if (!contractAddress || entry.params.contractAddress.toLowerCase() === contractAddress.toLowerCase()) {
|
|
43436
|
+
keysToRefresh.push(key);
|
|
43437
|
+
}
|
|
43438
|
+
}
|
|
43439
|
+
if (keysToRefresh.length === 0) {
|
|
43440
|
+
this.log(`No tracked balances affected by event for ${contractAddress || 'all contracts'}`);
|
|
43441
|
+
return;
|
|
43442
|
+
}
|
|
43443
|
+
// Debounce refresh per key
|
|
43444
|
+
for (const key of keysToRefresh) {
|
|
43445
|
+
const existing = this.pendingRefresh.get(key);
|
|
43446
|
+
if (existing) {
|
|
43447
|
+
clearTimeout(existing);
|
|
43448
|
+
}
|
|
43449
|
+
const timeout = setTimeout(async () => {
|
|
43450
|
+
this.pendingRefresh.delete(key);
|
|
43451
|
+
const entry = this.balances.get(key);
|
|
43452
|
+
if (entry) {
|
|
43453
|
+
this.log(`Refreshing ${key} due to wallet event: ${eventType}`);
|
|
43454
|
+
await this.refreshEntry(key, entry, eventType);
|
|
43455
|
+
}
|
|
43456
|
+
}, this.config.eventDebounceMs);
|
|
43457
|
+
this.pendingRefresh.set(key, timeout);
|
|
43458
|
+
}
|
|
43459
|
+
}
|
|
43460
|
+
/**
|
|
43461
|
+
* Fetch and cache a balance
|
|
43462
|
+
*/
|
|
43463
|
+
async fetchAndCache(params) {
|
|
43464
|
+
const key = this.getCacheKey(params);
|
|
43465
|
+
// Mark as refreshing to prevent duplicate fetches
|
|
43466
|
+
const existing = this.balances.get(key);
|
|
43467
|
+
if (existing?.refreshing) {
|
|
43468
|
+
// Wait for existing refresh to complete
|
|
43469
|
+
return existing.lastBalance || this.emptyBalance();
|
|
43470
|
+
}
|
|
43471
|
+
this.balances.set(key, {
|
|
43472
|
+
params,
|
|
43473
|
+
lastBalance: null,
|
|
43474
|
+
timestamp: Date.now(),
|
|
43475
|
+
refreshing: true
|
|
43476
|
+
});
|
|
43477
|
+
try {
|
|
43478
|
+
const balance = await this.tokenService.getTokenBalance({
|
|
43479
|
+
...params,
|
|
43480
|
+
tokenId: null // ERC-20 has no tokenId
|
|
43481
|
+
});
|
|
43482
|
+
this.balances.set(key, {
|
|
43483
|
+
params,
|
|
43484
|
+
lastBalance: balance,
|
|
43485
|
+
timestamp: Date.now(),
|
|
43486
|
+
refreshing: false
|
|
43487
|
+
});
|
|
43488
|
+
this.log(`Cached balance for ${key}: ${balance.balance}`);
|
|
43489
|
+
return balance;
|
|
43490
|
+
}
|
|
43491
|
+
catch (error) {
|
|
43492
|
+
// Remove failed entry
|
|
43493
|
+
this.balances.delete(key);
|
|
43494
|
+
throw error;
|
|
43495
|
+
}
|
|
43496
|
+
}
|
|
43497
|
+
/**
|
|
43498
|
+
* Refresh a cached entry and notify handlers
|
|
43499
|
+
*/
|
|
43500
|
+
async refreshEntry(key, entry, source) {
|
|
43501
|
+
if (entry.refreshing) {
|
|
43502
|
+
this.log(`Skipping refresh for ${key} - already refreshing`);
|
|
43503
|
+
return;
|
|
43504
|
+
}
|
|
43505
|
+
entry.refreshing = true;
|
|
43506
|
+
try {
|
|
43507
|
+
const newBalance = await this.tokenService.getTokenBalance({
|
|
43508
|
+
...entry.params,
|
|
43509
|
+
tokenId: null // ERC-20 has no tokenId
|
|
43510
|
+
});
|
|
43511
|
+
entry.lastBalance = newBalance;
|
|
43512
|
+
entry.timestamp = Date.now();
|
|
43513
|
+
entry.refreshing = false;
|
|
43514
|
+
this.log(`Refreshed ${key}: ${newBalance.balance}`);
|
|
43515
|
+
// Notify subscribers
|
|
43516
|
+
for (const handler of this.handlers) {
|
|
43517
|
+
try {
|
|
43518
|
+
handler(newBalance, entry.params.contractAddress, {
|
|
43519
|
+
type: source === 'refresh' ? 'refresh' : 'wallet_event',
|
|
43520
|
+
source
|
|
43521
|
+
});
|
|
43522
|
+
}
|
|
43523
|
+
catch (handlerError) {
|
|
43524
|
+
console.error('[BalanceManager] Handler error:', handlerError);
|
|
43525
|
+
}
|
|
43526
|
+
}
|
|
43527
|
+
}
|
|
43528
|
+
catch (error) {
|
|
43529
|
+
entry.refreshing = false;
|
|
43530
|
+
console.error(`[BalanceManager] Refresh failed for ${key}:`, error);
|
|
43531
|
+
}
|
|
43532
|
+
}
|
|
43533
|
+
/**
|
|
43534
|
+
* Generate cache key from params
|
|
43535
|
+
*/
|
|
43536
|
+
getCacheKey(params) {
|
|
43537
|
+
return `${params.contractAddress.toLowerCase()}:${params.accountAddress.toLowerCase()}`;
|
|
43538
|
+
}
|
|
43539
|
+
/**
|
|
43540
|
+
* Create empty balance placeholder
|
|
43541
|
+
*/
|
|
43542
|
+
emptyBalance() {
|
|
43543
|
+
return {
|
|
43544
|
+
tokenId: null,
|
|
43545
|
+
balance: 0,
|
|
43546
|
+
hasBalance: false,
|
|
43547
|
+
metadata: null
|
|
43548
|
+
};
|
|
43549
|
+
}
|
|
43550
|
+
/**
|
|
43551
|
+
* Debug logging
|
|
43552
|
+
*/
|
|
43553
|
+
log(message, data) {
|
|
43554
|
+
if (this.config.debug) {
|
|
43555
|
+
console.log(`[BalanceManager] ${message}`, data || '');
|
|
43556
|
+
}
|
|
43557
|
+
}
|
|
43558
|
+
/**
|
|
43559
|
+
* Cleanup resources
|
|
43560
|
+
*/
|
|
43561
|
+
destroy() {
|
|
43562
|
+
// Clear pending refreshes
|
|
43563
|
+
for (const timeout of this.pendingRefresh.values()) {
|
|
43564
|
+
clearTimeout(timeout);
|
|
43565
|
+
}
|
|
43566
|
+
this.pendingRefresh.clear();
|
|
43567
|
+
// Unsubscribe from events
|
|
43568
|
+
this.eventUnsubscribe?.();
|
|
43569
|
+
// Clear handlers and balances
|
|
43570
|
+
this.handlers.clear();
|
|
43571
|
+
this.balances.clear();
|
|
43572
|
+
this.log('Destroyed');
|
|
43573
|
+
}
|
|
43574
|
+
}
|
|
43575
|
+
|
|
41496
43576
|
/**
|
|
41497
43577
|
* Web3ApplicationService - Application layer entrance point
|
|
41498
43578
|
* Orchestrates domain services and provides clean public interface
|
|
@@ -41601,6 +43681,30 @@ class Web3InfrastructureApi {
|
|
|
41601
43681
|
return 0;
|
|
41602
43682
|
}
|
|
41603
43683
|
}
|
|
43684
|
+
/**
|
|
43685
|
+
* Get multiple ERC-1155 token balances in a single RPC call using native balanceOfBatch.
|
|
43686
|
+
* Falls back to individual calls if balanceOfBatch fails.
|
|
43687
|
+
*
|
|
43688
|
+
* @param request - Batch balance request
|
|
43689
|
+
* @returns Array of balance results per tokenId
|
|
43690
|
+
*/
|
|
43691
|
+
async getBatchTokenBalances(request) {
|
|
43692
|
+
try {
|
|
43693
|
+
const ethersProvider = await this.web3ChainService.getEthersProviderByChainId(request.chainId);
|
|
43694
|
+
const results = await getBatchBalancesWithFallback(ethersProvider, request.contractAddress, request.abi, request.accountAddress, request.tokenIds, request.isERC1155 ?? true);
|
|
43695
|
+
return results;
|
|
43696
|
+
}
|
|
43697
|
+
catch (error) {
|
|
43698
|
+
console.error(`Failed to get batch token balances:`, error);
|
|
43699
|
+
// Return empty results on failure
|
|
43700
|
+
return request.tokenIds.map(tokenId => ({
|
|
43701
|
+
tokenId,
|
|
43702
|
+
balance: 0n,
|
|
43703
|
+
success: false,
|
|
43704
|
+
error: `${error}`
|
|
43705
|
+
}));
|
|
43706
|
+
}
|
|
43707
|
+
}
|
|
41604
43708
|
async getTokenUri(request) {
|
|
41605
43709
|
try {
|
|
41606
43710
|
const ethersProvider = await this.web3ChainService.getEthersProviderByChainId(request.chainId);
|
|
@@ -41838,17 +43942,11 @@ async function getExplorerUrlByChainId(getChainData, chainId, address, type) {
|
|
|
41838
43942
|
* ```
|
|
41839
43943
|
*/
|
|
41840
43944
|
class Web3Manager {
|
|
41841
|
-
|
|
41842
|
-
// When ready, add:
|
|
41843
|
-
// 1. constructor param: private events?: PersEventEmitter
|
|
41844
|
-
// 2. Subscribe to contract events (Transfer, Approval, etc.)
|
|
41845
|
-
// 3. Emit via: this.events?.emitSuccess({ domain: 'web3', type: 'NFT_TRANSFERRED', ... })
|
|
41846
|
-
// 4. Filter to only user's address (not all transfers)
|
|
41847
|
-
// 5. Add cleanup for event listeners on SDK destroy
|
|
41848
|
-
constructor(apiClient, tenantManager) {
|
|
43945
|
+
constructor(apiClient, tenantManager, eventEmitter) {
|
|
41849
43946
|
this.apiClient = apiClient;
|
|
41850
43947
|
// Use provided TenantManager or create one
|
|
41851
43948
|
this.tenantManager = tenantManager || new TenantManager(apiClient);
|
|
43949
|
+
this.eventEmitter = eventEmitter;
|
|
41852
43950
|
// Initialize Web3 Chain service
|
|
41853
43951
|
const web3ChainApi = new Web3ChainApi(apiClient);
|
|
41854
43952
|
this.web3ChainService = new Web3ChainService(web3ChainApi);
|
|
@@ -41856,6 +43954,75 @@ class Web3Manager {
|
|
|
41856
43954
|
const web3InfrastructureApi = new Web3InfrastructureApi(this.web3ChainService);
|
|
41857
43955
|
const ipfsInfrastructureApi = new IPFSInfrastructureApi(this.tenantManager);
|
|
41858
43956
|
this.web3ApplicationService = new Web3ApplicationService(web3InfrastructureApi, ipfsInfrastructureApi);
|
|
43957
|
+
// Initialize domain services for managers
|
|
43958
|
+
const contractDomainService = new ContractDomainService();
|
|
43959
|
+
const metadataDomainService = new MetadataDomainService(ipfsInfrastructureApi);
|
|
43960
|
+
this.tokenDomainService = new TokenDomainService(web3InfrastructureApi, metadataDomainService, contractDomainService);
|
|
43961
|
+
}
|
|
43962
|
+
/**
|
|
43963
|
+
* Get the TokenCollectionManager for NFT collection operations.
|
|
43964
|
+
*
|
|
43965
|
+
* Features:
|
|
43966
|
+
* - Auto-refresh on wallet events (transfers, mints, burns)
|
|
43967
|
+
* - Cached collections
|
|
43968
|
+
* - Subscriber pattern for reactive updates
|
|
43969
|
+
*
|
|
43970
|
+
* @example
|
|
43971
|
+
* ```typescript
|
|
43972
|
+
* const manager = sdk.web3.collectionManager;
|
|
43973
|
+
*
|
|
43974
|
+
* // Subscribe to updates
|
|
43975
|
+
* const unsubscribe = manager.subscribe((collection, contractAddress) => {
|
|
43976
|
+
* console.log(`Collection updated for ${contractAddress}`);
|
|
43977
|
+
* });
|
|
43978
|
+
*
|
|
43979
|
+
* // Fetch collection (auto-cached, auto-refreshed)
|
|
43980
|
+
* const collection = await manager.getCollection({...});
|
|
43981
|
+
*
|
|
43982
|
+
* // Manual refresh
|
|
43983
|
+
* await manager.refreshCollection(contractAddress);
|
|
43984
|
+
* ```
|
|
43985
|
+
*/
|
|
43986
|
+
get collectionManager() {
|
|
43987
|
+
if (!this._collectionManager) {
|
|
43988
|
+
this._collectionManager = new TokenCollectionManager(this.tokenDomainService, this.eventEmitter);
|
|
43989
|
+
}
|
|
43990
|
+
return this._collectionManager;
|
|
43991
|
+
}
|
|
43992
|
+
/**
|
|
43993
|
+
* Get the BalanceManager for ERC-20 (credit token) balance operations.
|
|
43994
|
+
*
|
|
43995
|
+
* Features:
|
|
43996
|
+
* - Auto-refresh on wallet events (transfers)
|
|
43997
|
+
* - Cached balances
|
|
43998
|
+
* - Subscriber pattern for reactive updates
|
|
43999
|
+
*
|
|
44000
|
+
* @example
|
|
44001
|
+
* ```typescript
|
|
44002
|
+
* const manager = sdk.web3.balanceManager;
|
|
44003
|
+
*
|
|
44004
|
+
* // Subscribe to balance updates
|
|
44005
|
+
* const unsubscribe = manager.subscribe((balance, contractAddress) => {
|
|
44006
|
+
* console.log(`Balance updated: ${balance.balance}`);
|
|
44007
|
+
* });
|
|
44008
|
+
*
|
|
44009
|
+
* // Fetch balance (auto-cached, auto-refreshed)
|
|
44010
|
+
* const balance = await manager.getBalance({
|
|
44011
|
+
* accountAddress: '0x...',
|
|
44012
|
+
* contractAddress: '0x...',
|
|
44013
|
+
* abi: creditTokenAbi,
|
|
44014
|
+
* chainId: 39123
|
|
44015
|
+
* });
|
|
44016
|
+
*
|
|
44017
|
+
* // Manual refresh
|
|
44018
|
+
* await manager.refreshBalance(contractAddress);
|
|
44019
|
+
* ```
|
|
44020
|
+
*/
|
|
44021
|
+
get balanceManager() {
|
|
44022
|
+
if (!this._balanceManager) {
|
|
44023
|
+
this._balanceManager = new BalanceManager(this.tokenDomainService, this.eventEmitter);
|
|
44024
|
+
}
|
|
44025
|
+
return this._balanceManager;
|
|
41859
44026
|
}
|
|
41860
44027
|
/**
|
|
41861
44028
|
* Get token balance for a specific token
|
|
@@ -42054,6 +44221,18 @@ class Web3Manager {
|
|
|
42054
44221
|
maxTokens
|
|
42055
44222
|
};
|
|
42056
44223
|
}
|
|
44224
|
+
/**
|
|
44225
|
+
* Cleanup resources.
|
|
44226
|
+
*
|
|
44227
|
+
* Call this when the SDK is being destroyed to clean up
|
|
44228
|
+
* event subscriptions and cached data.
|
|
44229
|
+
*/
|
|
44230
|
+
destroy() {
|
|
44231
|
+
this._collectionManager?.destroy();
|
|
44232
|
+
this._balanceManager?.destroy();
|
|
44233
|
+
this._collectionManager = undefined;
|
|
44234
|
+
this._balanceManager = undefined;
|
|
44235
|
+
}
|
|
42057
44236
|
}
|
|
42058
44237
|
|
|
42059
44238
|
/**
|
|
@@ -42945,6 +45124,83 @@ class BrowserFetchClientAdapter {
|
|
|
42945
45124
|
}
|
|
42946
45125
|
}
|
|
42947
45126
|
|
|
45127
|
+
/**
|
|
45128
|
+
* Build optimized image URL with automatic IPFS resolution and CDN options
|
|
45129
|
+
*
|
|
45130
|
+
* Single entry point for all image URL building:
|
|
45131
|
+
* - IPFS URIs → resolved via gateway + CDN options applied
|
|
45132
|
+
* - HTTP URLs → CDN options applied directly
|
|
45133
|
+
*
|
|
45134
|
+
* @param url - Image URL (ipfs://, https://, or any URL)
|
|
45135
|
+
* @param options - CDN optimization options (preset, quality, format, fit)
|
|
45136
|
+
* @param gateway - IPFS gateway domain (required for ipfs:// URLs)
|
|
45137
|
+
* @returns Optimized URL with query parameters
|
|
45138
|
+
*
|
|
45139
|
+
* @example
|
|
45140
|
+
* ```typescript
|
|
45141
|
+
* // IPFS thumbnail
|
|
45142
|
+
* buildImageUrl('ipfs://QmHash', { preset: 'thumb' }, 'cdn.example.com')
|
|
45143
|
+
* // => 'https://cdn.example.com/ipfs/QmHash?preset=thumb'
|
|
45144
|
+
*
|
|
45145
|
+
* // IPFS hero with high quality
|
|
45146
|
+
* buildImageUrl('ipfs://QmHash', { preset: 'hero', q: 95, fit: 'cover' }, 'cdn.example.com')
|
|
45147
|
+
* // => 'https://cdn.example.com/ipfs/QmHash?preset=hero&q=95&fit=cover'
|
|
45148
|
+
*
|
|
45149
|
+
* // HTTP URL (gateway ignored)
|
|
45150
|
+
* buildImageUrl('https://example.com/photo.jpg', { preset: 'card' })
|
|
45151
|
+
* // => 'https://example.com/photo.jpg?preset=card'
|
|
45152
|
+
*
|
|
45153
|
+
* // No options - just IPFS resolution
|
|
45154
|
+
* buildImageUrl('ipfs://QmHash', undefined, 'cdn.example.com')
|
|
45155
|
+
* // => 'https://cdn.example.com/ipfs/QmHash'
|
|
45156
|
+
* ```
|
|
45157
|
+
*/
|
|
45158
|
+
function buildImageUrl(url, options, gateway) {
|
|
45159
|
+
if (!url)
|
|
45160
|
+
return '';
|
|
45161
|
+
let resolvedUrl = url;
|
|
45162
|
+
// Resolve IPFS to HTTP via gateway
|
|
45163
|
+
if (url.startsWith('ipfs://')) {
|
|
45164
|
+
if (!gateway) {
|
|
45165
|
+
console.warn('[buildImageUrl] IPFS URL requires gateway parameter');
|
|
45166
|
+
return url;
|
|
45167
|
+
}
|
|
45168
|
+
const cid = url.replace('ipfs://', '');
|
|
45169
|
+
resolvedUrl = `https://${gateway}/ipfs/${cid}`;
|
|
45170
|
+
}
|
|
45171
|
+
// Apply CDN options
|
|
45172
|
+
return buildImageCdnUrl(resolvedUrl, options);
|
|
45173
|
+
}
|
|
45174
|
+
/**
|
|
45175
|
+
* Check if a URL is an IPFS URI
|
|
45176
|
+
*
|
|
45177
|
+
* @param url - URL to check
|
|
45178
|
+
* @returns True if URL starts with ipfs://
|
|
45179
|
+
*/
|
|
45180
|
+
function isIpfsUrl(url) {
|
|
45181
|
+
return !!url && url.startsWith('ipfs://');
|
|
45182
|
+
}
|
|
45183
|
+
/**
|
|
45184
|
+
* Extract CID from IPFS URI
|
|
45185
|
+
*
|
|
45186
|
+
* @param ipfsUri - IPFS URI (ipfs://Qm... or ipfs://baf...)
|
|
45187
|
+
* @returns CID string, or null if not a valid IPFS URI
|
|
45188
|
+
*
|
|
45189
|
+
* @example
|
|
45190
|
+
* ```typescript
|
|
45191
|
+
* extractIpfsCid('ipfs://QmHash123')
|
|
45192
|
+
* // => 'QmHash123'
|
|
45193
|
+
*
|
|
45194
|
+
* extractIpfsCid('https://example.com/image.jpg')
|
|
45195
|
+
* // => null
|
|
45196
|
+
* ```
|
|
45197
|
+
*/
|
|
45198
|
+
function extractIpfsCid(ipfsUri) {
|
|
45199
|
+
if (!isIpfsUrl(ipfsUri))
|
|
45200
|
+
return null;
|
|
45201
|
+
return ipfsUri.replace('ipfs://', '');
|
|
45202
|
+
}
|
|
45203
|
+
|
|
42948
45204
|
/**
|
|
42949
45205
|
* React hook for loading and managing token balances
|
|
42950
45206
|
*
|
|
@@ -46619,8 +48875,11 @@ const useTriggerSources = () => {
|
|
|
46619
48875
|
};
|
|
46620
48876
|
};
|
|
46621
48877
|
|
|
48878
|
+
exports.ACTIVE_USER_DEFINITION = ACTIVE_USER_DEFINITION;
|
|
46622
48879
|
exports.ANALYTICS_GROUP_BY_TYPES = ANALYTICS_GROUP_BY_TYPES;
|
|
46623
48880
|
exports.ANALYTICS_METRIC_TYPES = ANALYTICS_METRIC_TYPES;
|
|
48881
|
+
exports.ANALYTICS_VALID_PROCESS_STATUS = ANALYTICS_VALID_PROCESS_STATUS;
|
|
48882
|
+
exports.ANALYTICS_VALID_TRANSACTION_STATUSES = ANALYTICS_VALID_TRANSACTION_STATUSES;
|
|
46624
48883
|
exports.AUTH_STORAGE_KEYS = AUTH_STORAGE_KEYS;
|
|
46625
48884
|
exports.AnalyticsApi = AnalyticsApi;
|
|
46626
48885
|
exports.AnalyticsManager = AnalyticsManager;
|
|
@@ -46651,6 +48910,7 @@ exports.CampaignService = CampaignService;
|
|
|
46651
48910
|
exports.ChainTypes = ChainTypes;
|
|
46652
48911
|
exports.ClientTransactionType = ClientTransactionType;
|
|
46653
48912
|
exports.CommonErrorCodes = CommonErrorCodes;
|
|
48913
|
+
exports.CustomFieldDefinitionManager = CustomFieldDefinitionManager;
|
|
46654
48914
|
exports.DEFAULT_PERS_CONFIG = DEFAULT_PERS_CONFIG;
|
|
46655
48915
|
exports.DPOP_STORAGE_KEYS = DPOP_STORAGE_KEYS;
|
|
46656
48916
|
exports.DPoPManager = DPoPManager;
|
|
@@ -46659,6 +48919,9 @@ exports.Domains = Domains;
|
|
|
46659
48919
|
exports.DonationApi = DonationApi;
|
|
46660
48920
|
exports.DonationManager = DonationManager;
|
|
46661
48921
|
exports.DonationService = DonationService;
|
|
48922
|
+
exports.EMAIL_MAX_LENGTH = EMAIL_MAX_LENGTH;
|
|
48923
|
+
exports.EMAIL_REGEX = EMAIL_REGEX;
|
|
48924
|
+
exports.ENGAGEMENT_RATE_FORMULA = ENGAGEMENT_RATE_FORMULA;
|
|
46662
48925
|
exports.EntityTypes = EntityTypes;
|
|
46663
48926
|
exports.ErrorDomains = ErrorDomains;
|
|
46664
48927
|
exports.ErrorUtils = ErrorUtils;
|
|
@@ -46666,6 +48929,14 @@ exports.FATAL_AUTH_CODES = FATAL_AUTH_CODES;
|
|
|
46666
48929
|
exports.FileApi = FileApi;
|
|
46667
48930
|
exports.FileManager = FileManager;
|
|
46668
48931
|
exports.FileService = FileService;
|
|
48932
|
+
exports.IMAGE_CDN_CACHE_TTL = IMAGE_CDN_CACHE_TTL;
|
|
48933
|
+
exports.IMAGE_CDN_DEFAULT_FIT = IMAGE_CDN_DEFAULT_FIT;
|
|
48934
|
+
exports.IMAGE_CDN_DEFAULT_FORMAT = IMAGE_CDN_DEFAULT_FORMAT;
|
|
48935
|
+
exports.IMAGE_CDN_DEFAULT_QUALITY = IMAGE_CDN_DEFAULT_QUALITY;
|
|
48936
|
+
exports.IMAGE_CDN_MAX_DIMENSION = IMAGE_CDN_MAX_DIMENSION;
|
|
48937
|
+
exports.IMAGE_CDN_MAX_INPUT_SIZE = IMAGE_CDN_MAX_INPUT_SIZE;
|
|
48938
|
+
exports.IMAGE_CDN_PRESETS = IMAGE_CDN_PRESETS;
|
|
48939
|
+
exports.IMAGE_CDN_SUPPORTED_FORMATS = IMAGE_CDN_SUPPORTED_FORMATS;
|
|
46669
48940
|
exports.IndexedDBTokenStorage = IndexedDBTokenStorage;
|
|
46670
48941
|
exports.LocalStorageTokenStorage = LocalStorageTokenStorage;
|
|
46671
48942
|
exports.LogoutRequired = LogoutRequired;
|
|
@@ -46674,6 +48945,7 @@ exports.MemoryTokenStorage = MemoryTokenStorage;
|
|
|
46674
48945
|
exports.NativeTokenTypes = NativeTokenTypes;
|
|
46675
48946
|
exports.NetworkError = NetworkError;
|
|
46676
48947
|
exports.NodeHttpClientAdapter = NodeHttpClientAdapter;
|
|
48948
|
+
exports.OttAuthMethod = OttAuthMethod;
|
|
46677
48949
|
exports.PaymentApi = PurchaseApi;
|
|
46678
48950
|
exports.PaymentService = PaymentService;
|
|
46679
48951
|
exports.PersApiClient = PersApiClient;
|
|
@@ -46718,6 +48990,8 @@ exports.TransactionService = TransactionService;
|
|
|
46718
48990
|
exports.TriggerSourceApi = TriggerSourceApi;
|
|
46719
48991
|
exports.TriggerSourceManager = TriggerSourceManager;
|
|
46720
48992
|
exports.TriggerSourceService = TriggerSourceService;
|
|
48993
|
+
exports.URL_REGEX = URL_REGEX;
|
|
48994
|
+
exports.UUID_REGEX = UUID_REGEX;
|
|
46721
48995
|
exports.UserApi = UserApi;
|
|
46722
48996
|
exports.UserManager = UserManager;
|
|
46723
48997
|
exports.UserService = UserService;
|
|
@@ -46730,8 +49004,10 @@ exports.VALID_CAMPAIGN_RELATIONS = VALID_CAMPAIGN_RELATIONS;
|
|
|
46730
49004
|
exports.VALID_REDEMPTION_REDEEM_RELATIONS = VALID_REDEMPTION_REDEEM_RELATIONS;
|
|
46731
49005
|
exports.VALID_REDEMPTION_RELATIONS = VALID_REDEMPTION_RELATIONS;
|
|
46732
49006
|
exports.VALID_RELATIONS = VALID_RELATIONS;
|
|
49007
|
+
exports.VALID_TOKEN_METADATA_RELATIONS = VALID_TOKEN_METADATA_RELATIONS;
|
|
46733
49008
|
exports.VALID_TRANSACTION_RELATIONS = VALID_TRANSACTION_RELATIONS;
|
|
46734
49009
|
exports.VALID_USER_RELATIONS = VALID_USER_RELATIONS;
|
|
49010
|
+
exports.WALLET_ADDRESS_REGEX = WALLET_ADDRESS_REGEX;
|
|
46735
49011
|
exports.WalletEventsManager = WalletEventsManager;
|
|
46736
49012
|
exports.WebDPoPCryptoProvider = WebDPoPCryptoProvider;
|
|
46737
49013
|
exports.WebhookApi = WebhookApi;
|
|
@@ -46740,6 +49016,8 @@ exports.WebhookService = WebhookService;
|
|
|
46740
49016
|
exports.apiPublicKeyTestPrefix = apiPublicKeyTestPrefix;
|
|
46741
49017
|
exports.buildApiRoot = buildApiRoot;
|
|
46742
49018
|
exports.buildBurnRequest = buildBurnRequest;
|
|
49019
|
+
exports.buildImageCdnUrl = buildImageCdnUrl;
|
|
49020
|
+
exports.buildImageUrl = buildImageUrl;
|
|
46743
49021
|
exports.buildMintRequest = buildMintRequest;
|
|
46744
49022
|
exports.buildPOSBurnRequest = buildPOSBurnRequest;
|
|
46745
49023
|
exports.buildPOSTransferRequest = buildPOSTransferRequest;
|
|
@@ -46748,46 +49026,67 @@ exports.buildPendingTransactionData = buildPendingTransactionData;
|
|
|
46748
49026
|
exports.buildSubmissionRequest = buildSubmissionRequest;
|
|
46749
49027
|
exports.buildTransferRequest = buildTransferRequest;
|
|
46750
49028
|
exports.buildWalletEventsWsUrl = buildWalletEventsWsUrl;
|
|
49029
|
+
exports.builtInUserFieldMetadata = builtInUserFieldMetadata;
|
|
49030
|
+
exports.builtInUserFields = builtInUserFields;
|
|
49031
|
+
exports.compareValues = compareValues;
|
|
46751
49032
|
exports.createPersEventsClient = createPersEventsClient;
|
|
46752
49033
|
exports.createPersSDK = createPersSDK;
|
|
46753
49034
|
exports.createReactNativeAuthProvider = createReactNativeAuthProvider;
|
|
46754
49035
|
exports.createUserIdentifierObject = createUserIdentifierObject;
|
|
46755
49036
|
exports.createUserStatusSDK = createUserStatusSDK;
|
|
49037
|
+
exports.customFieldEntityTypes = customFieldEntityTypes;
|
|
46756
49038
|
exports.detectEnvironment = detectEnvironment;
|
|
46757
49039
|
exports.detectIdentifierType = detectIdentifierType;
|
|
46758
49040
|
exports.environment = environment;
|
|
46759
49041
|
exports.extractData = extractData;
|
|
46760
49042
|
exports.extractDeadlineFromSigningData = extractDeadlineFromSigningData;
|
|
49043
|
+
exports.extractIpfsCid = extractIpfsCid;
|
|
46761
49044
|
exports.extractPagination = extractPagination;
|
|
46762
49045
|
exports.fetchAllPages = fetchAllPages;
|
|
49046
|
+
exports.getAllBuiltInFieldDefinitions = getAllBuiltInFieldDefinitions;
|
|
49047
|
+
exports.getBuiltInFieldDefinition = getBuiltInFieldDefinition;
|
|
46763
49048
|
exports.getMetadataFromTokenUnitResponse = getMetadataFromTokenUnitResponse;
|
|
49049
|
+
exports.getOperatorText = getOperatorText;
|
|
49050
|
+
exports.getPresetDimensions = getPresetDimensions;
|
|
46764
49051
|
exports.getSigningUrl = getSigningUrl;
|
|
46765
49052
|
exports.globalCacheService = globalCacheService;
|
|
46766
49053
|
exports.hasMinimumRole = hasMinimumRole;
|
|
46767
49054
|
exports.initializeReactNativePolyfills = initializeReactNativePolyfills;
|
|
49055
|
+
exports.isBuiltInUserField = isBuiltInUserField;
|
|
46768
49056
|
exports.isConnectedMessage = isConnectedMessage;
|
|
46769
49057
|
exports.isErrorMessage = isErrorMessage;
|
|
46770
49058
|
exports.isEventMessage = isEventMessage;
|
|
46771
49059
|
exports.isFatalAuthErrorInMessage = isFatalAuthErrorInMessage;
|
|
49060
|
+
exports.isIpfsUrl = isIpfsUrl;
|
|
46772
49061
|
exports.isPaginatedResponse = isPaginatedResponse;
|
|
46773
49062
|
exports.isPingMessage = isPingMessage;
|
|
46774
49063
|
exports.isServerMessage = isServerMessage;
|
|
46775
49064
|
exports.isSubscribedMessage = isSubscribedMessage;
|
|
49065
|
+
exports.isSupportedImageFormat = isSupportedImageFormat;
|
|
49066
|
+
exports.isUUID = isUUID;
|
|
46776
49067
|
exports.isUnsubscribedMessage = isUnsubscribedMessage;
|
|
46777
49068
|
exports.isUserIdentifierObject = isUserIdentifierObject;
|
|
46778
49069
|
exports.isValidBusinessMembershipRelation = isValidBusinessMembershipRelation;
|
|
46779
49070
|
exports.isValidCampaignClaimRelation = isValidCampaignClaimRelation;
|
|
46780
49071
|
exports.isValidCampaignRelation = isValidCampaignRelation;
|
|
49072
|
+
exports.isValidEmail = isValidEmail;
|
|
49073
|
+
exports.isValidNonEmptyString = isValidNonEmptyString;
|
|
49074
|
+
exports.isValidPreset = isValidPreset;
|
|
46781
49075
|
exports.isValidRedemptionRedeemRelation = isValidRedemptionRedeemRelation;
|
|
46782
49076
|
exports.isValidRedemptionRelation = isValidRedemptionRelation;
|
|
46783
49077
|
exports.isValidRelation = isValidRelation;
|
|
49078
|
+
exports.isValidTokenMetadataRelation = isValidTokenMetadataRelation;
|
|
46784
49079
|
exports.isValidTransactionRelation = isValidTransactionRelation;
|
|
49080
|
+
exports.isValidUrl = isValidUrl;
|
|
46785
49081
|
exports.isValidUserRelation = isValidUserRelation;
|
|
49082
|
+
exports.isValidUuid = isValidUuid;
|
|
49083
|
+
exports.isValidWalletAddress = isValidWalletAddress;
|
|
46786
49084
|
exports.mergeWithDefaults = mergeWithDefaults;
|
|
46787
49085
|
exports.needsExternalSigning = needsExternalSigning;
|
|
46788
49086
|
exports.needsSubmission = needsSubmission;
|
|
46789
49087
|
exports.normalizeFilters = normalizeFilters;
|
|
46790
49088
|
exports.normalizeToPaginated = normalizeToPaginated;
|
|
49089
|
+
exports.operatorText = operatorText;
|
|
46791
49090
|
exports.registerIdentifierType = registerIdentifierType;
|
|
46792
49091
|
exports.testnetPrefix = testnetPrefix;
|
|
46793
49092
|
exports.testnetShortPrefix = testnetShortPrefix;
|
|
@@ -46815,5 +49114,10 @@ exports.useTriggerSources = useTriggerSources;
|
|
|
46815
49114
|
exports.useUserStatus = useUserStatus;
|
|
46816
49115
|
exports.useUsers = useUsers;
|
|
46817
49116
|
exports.useWeb3 = useWeb3;
|
|
49117
|
+
exports.userModelFields = userModelFields;
|
|
49118
|
+
exports.validateAllFields = validateAllFields;
|
|
49119
|
+
exports.validateFieldComparisons = validateFieldComparisons;
|
|
49120
|
+
exports.validateFieldValue = validateFieldValue;
|
|
49121
|
+
exports.validateObjectArrayItems = validateObjectArrayItems;
|
|
46818
49122
|
exports.warnIfProblematicEnvironment = warnIfProblematicEnvironment;
|
|
46819
49123
|
//# sourceMappingURL=index.js.map
|