@taskon/core 0.0.1-beta.1 → 0.0.1-beta.3
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.cjs +528 -151
- package/dist/index.d.ts +294 -87
- package/dist/index.js +526 -151
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -195,9 +195,14 @@ function createTaskOnClient(config) {
|
|
|
195
195
|
if (userToken) {
|
|
196
196
|
mergedHeaders["Authorization"] = userToken;
|
|
197
197
|
}
|
|
198
|
-
// Setup timeout
|
|
198
|
+
// Setup timeout.
|
|
199
|
+
// We keep a local flag so abort errors can be classified as timeout vs external abort.
|
|
199
200
|
const controller = new AbortController();
|
|
200
|
-
|
|
201
|
+
let isTimedOut = false;
|
|
202
|
+
const timeoutId = setTimeout(() => {
|
|
203
|
+
isTimedOut = true;
|
|
204
|
+
controller.abort();
|
|
205
|
+
}, timeout);
|
|
201
206
|
const signal = externalSignal
|
|
202
207
|
? combineSignals(externalSignal, controller.signal)
|
|
203
208
|
: controller.signal;
|
|
@@ -213,13 +218,53 @@ function createTaskOnClient(config) {
|
|
|
213
218
|
}
|
|
214
219
|
catch (err) {
|
|
215
220
|
clearTimeout(timeoutId);
|
|
216
|
-
|
|
221
|
+
// Normalize transport-layer errors into ApiError so all callers can rely on `code`.
|
|
222
|
+
if (err instanceof ApiError) {
|
|
223
|
+
throw err;
|
|
224
|
+
}
|
|
225
|
+
const isAbortError = err instanceof DOMException
|
|
226
|
+
? err.name === "AbortError"
|
|
227
|
+
: err instanceof Error
|
|
228
|
+
? err.name === "AbortError"
|
|
229
|
+
: false;
|
|
230
|
+
if (isAbortError) {
|
|
231
|
+
if (isTimedOut) {
|
|
232
|
+
throw new ApiError("TIMEOUT", `Request timed out after ${timeout}ms`, { timeout, url: fullURL });
|
|
233
|
+
}
|
|
234
|
+
throw new ApiError("REQUEST_ABORTED", "Request was aborted", {
|
|
235
|
+
url: fullURL,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
throw new ApiError("NETWORK_ERROR", err instanceof Error ? err.message : "Network request failed", { url: fullURL, cause: err });
|
|
217
239
|
}
|
|
218
240
|
}
|
|
219
241
|
async function request(options) {
|
|
220
242
|
// Execute request
|
|
221
243
|
const response = await executeRequest(options);
|
|
222
|
-
|
|
244
|
+
let result = null;
|
|
245
|
+
// Best-effort JSON parsing:
|
|
246
|
+
// - For non-2xx responses we still want to extract backend error payload if present.
|
|
247
|
+
// - For 2xx responses, invalid JSON should be treated as protocol error.
|
|
248
|
+
try {
|
|
249
|
+
result = (await response.json());
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
result = null;
|
|
253
|
+
}
|
|
254
|
+
// Fail fast on HTTP non-2xx.
|
|
255
|
+
// fetch() does not throw for HTTP errors, so we must convert them into ApiError manually.
|
|
256
|
+
if (!response.ok) {
|
|
257
|
+
const httpError = new ApiError(result?.error?.code ?? `HTTP_${response.status}`, result?.error?.message ??
|
|
258
|
+
`Request failed with HTTP status ${response.status}`, result?.error?.data);
|
|
259
|
+
if (onAuthError && (response.status === 403 || isAuthError(httpError.code))) {
|
|
260
|
+
onAuthError(httpError);
|
|
261
|
+
}
|
|
262
|
+
throw httpError;
|
|
263
|
+
}
|
|
264
|
+
// HTTP is 2xx but body is not valid JSON: treat as invalid response shape.
|
|
265
|
+
if (!result) {
|
|
266
|
+
throw new ApiError("INVALID_RESPONSE", "Invalid response format: expected JSON body");
|
|
267
|
+
}
|
|
223
268
|
// Check for auth error (HTTP 403 or specific error codes like NEED_LOGIN, INVALID_USER)
|
|
224
269
|
if (onAuthError) {
|
|
225
270
|
if (response.status === 403 || isAuthError(result.error?.code)) {
|
|
@@ -1340,14 +1385,15 @@ function normalizeTwitterReply(task) {
|
|
|
1340
1385
|
const username = params.twitter_handle || "";
|
|
1341
1386
|
const desc = params.desc || "";
|
|
1342
1387
|
const tagEnabled = params.tag_enabled || false;
|
|
1343
|
-
const friendsCount = params.friends_count || 0;
|
|
1344
1388
|
// 生成推文链接
|
|
1345
1389
|
const tweetLink = username
|
|
1346
1390
|
? getTweetUrl(username, tweetId)
|
|
1347
1391
|
: `https://x.com/i/status/${tweetId}`;
|
|
1348
1392
|
// 根据 tag_enabled 确定标题
|
|
1393
|
+
// 与 Vue 原版保持一致:ReplyTweet 的好友数量文案固定为 "3 friends"
|
|
1394
|
+
// 不读取 friends_count,避免出现 "0 friends" 等与原版不一致的展示
|
|
1349
1395
|
const customTitle = tagEnabled
|
|
1350
|
-
?
|
|
1396
|
+
? "Reply tweet & tag 3 friends"
|
|
1351
1397
|
: "Reply tweet";
|
|
1352
1398
|
// Reply 任务使用推文链接作为 action,用户点击后手动回复
|
|
1353
1399
|
const templateValue = {
|
|
@@ -1368,7 +1414,13 @@ function normalizeTwitterReply(task) {
|
|
|
1368
1414
|
: [],
|
|
1369
1415
|
user_identity_type: "Twitter",
|
|
1370
1416
|
};
|
|
1371
|
-
|
|
1417
|
+
// 与 Vue 原版 Reply 专用组件逻辑保持一致:
|
|
1418
|
+
// Reply 系列标题不使用运营编辑后的 task.name,统一回退到 template_name。
|
|
1419
|
+
const result = normalizeTaskParams(task, templateValue);
|
|
1420
|
+
return {
|
|
1421
|
+
...result,
|
|
1422
|
+
name: task.template.template_name,
|
|
1423
|
+
};
|
|
1372
1424
|
}
|
|
1373
1425
|
/**
|
|
1374
1426
|
* 标准化 Twitter Reply with HashTag 任务
|
|
@@ -1397,14 +1449,15 @@ function normalizeTwitterReplyHashTag(task) {
|
|
|
1397
1449
|
const hashTag = params.hash_tag || "";
|
|
1398
1450
|
const desc = params.desc || "";
|
|
1399
1451
|
const tagEnabled = params.tag_enabled || false;
|
|
1400
|
-
const friendsCount = params.friends_count || 0;
|
|
1401
1452
|
// 格式化 Hashtag
|
|
1402
1453
|
const formattedHashTag = formatHashtagLabel(hashTag);
|
|
1403
1454
|
// 生成推文链接
|
|
1404
1455
|
const tweetLink = getTweetUrl(username, tweetId);
|
|
1405
1456
|
// 根据 tag_enabled 确定标题
|
|
1457
|
+
// 与 Vue 原版保持一致:ReplyTweetAndHashTag 的好友数量文案固定为 "3 friends"
|
|
1458
|
+
// 不读取 friends_count,避免出现与原版不一致的动态人数
|
|
1406
1459
|
const customTitle = tagEnabled
|
|
1407
|
-
?
|
|
1460
|
+
? "Reply tweet with hashtag & tag 3 friends"
|
|
1408
1461
|
: "Reply tweet with hashtag";
|
|
1409
1462
|
const templateValue = {
|
|
1410
1463
|
custom_title: customTitle,
|
|
@@ -1422,7 +1475,13 @@ function normalizeTwitterReplyHashTag(task) {
|
|
|
1422
1475
|
: [],
|
|
1423
1476
|
user_identity_type: "Twitter",
|
|
1424
1477
|
};
|
|
1425
|
-
|
|
1478
|
+
// 与 Vue 原版 Reply 专用组件逻辑保持一致:
|
|
1479
|
+
// Reply 系列标题不使用运营编辑后的 task.name,统一回退到 template_name。
|
|
1480
|
+
const result = normalizeTaskParams(task, templateValue);
|
|
1481
|
+
return {
|
|
1482
|
+
...result,
|
|
1483
|
+
name: task.template.template_name,
|
|
1484
|
+
};
|
|
1426
1485
|
}
|
|
1427
1486
|
/**
|
|
1428
1487
|
* 标准化 Twitter Quote 任务
|
|
@@ -1668,16 +1727,7 @@ function normalizeTelegramJoin(task) {
|
|
|
1668
1727
|
user_identity_type: "Telegram",
|
|
1669
1728
|
is_link_task: !needVerifyBot, // 不需要验证时等同于链接任务
|
|
1670
1729
|
};
|
|
1671
|
-
|
|
1672
|
-
// Vue 版本 TelegramJoin.vue 无条件使用富文本渲染 desc
|
|
1673
|
-
// 这里强制设置 is_rich_text: true 以保持一致
|
|
1674
|
-
return {
|
|
1675
|
-
...normalized,
|
|
1676
|
-
template: {
|
|
1677
|
-
...normalized.template,
|
|
1678
|
-
is_rich_text: true,
|
|
1679
|
-
},
|
|
1680
|
-
};
|
|
1730
|
+
return normalizeTaskParams(task, templateValue);
|
|
1681
1731
|
}
|
|
1682
1732
|
|
|
1683
1733
|
/**
|
|
@@ -1783,10 +1833,7 @@ function normalizeOuterAPI$1(task) {
|
|
|
1783
1833
|
? SNS_TYPE_MAP[params.sns_type]
|
|
1784
1834
|
: undefined,
|
|
1785
1835
|
};
|
|
1786
|
-
|
|
1787
|
-
const normalized = normalizeTaskParams(task, normalizedParams);
|
|
1788
|
-
normalized.template.is_rich_text = true;
|
|
1789
|
-
return normalized;
|
|
1836
|
+
return normalizeTaskParams(task, normalizedParams);
|
|
1790
1837
|
}
|
|
1791
1838
|
/**
|
|
1792
1839
|
* 标准化 OuterPointAPI 任务
|
|
@@ -1843,10 +1890,7 @@ function normalizeOuterPointAPI(task) {
|
|
|
1843
1890
|
? SNS_TYPE_MAP[params.sns_type]
|
|
1844
1891
|
: undefined,
|
|
1845
1892
|
};
|
|
1846
|
-
|
|
1847
|
-
const normalized = normalizeTaskParams(task, normalizedParams);
|
|
1848
|
-
normalized.template.is_rich_text = true;
|
|
1849
|
-
return normalized;
|
|
1893
|
+
return normalizeTaskParams(task, normalizedParams);
|
|
1850
1894
|
}
|
|
1851
1895
|
|
|
1852
1896
|
/**
|
|
@@ -2211,6 +2255,17 @@ function createCommunityTaskApi(client) {
|
|
|
2211
2255
|
// Normalize task cards to standardize simple tasks into common template format
|
|
2212
2256
|
return normalizeTaskCards(res.result);
|
|
2213
2257
|
},
|
|
2258
|
+
/**
|
|
2259
|
+
* 获取单个社区任务卡片详情
|
|
2260
|
+
* POST /community/v1/getCommunityTaskCardInfo
|
|
2261
|
+
*/
|
|
2262
|
+
async getCommunityTaskCardInfo(params) {
|
|
2263
|
+
const res = await client.request({
|
|
2264
|
+
url: "/community/v1/getCommunityTaskCardInfo",
|
|
2265
|
+
data: params,
|
|
2266
|
+
});
|
|
2267
|
+
return normalizeTaskCard(res.result);
|
|
2268
|
+
},
|
|
2214
2269
|
/**
|
|
2215
2270
|
* 提交/验证任务
|
|
2216
2271
|
* POST /community/v1/submitTask
|
|
@@ -3577,13 +3632,14 @@ function normalizeReplyTweet(task) {
|
|
|
3577
3632
|
const tweetId = params.tweet_id || "";
|
|
3578
3633
|
const desc = params.desc || "";
|
|
3579
3634
|
const tagEnabled = params.tag_enabled;
|
|
3580
|
-
const friendsCount = params.friends_count || 3;
|
|
3581
3635
|
// 使用 Twitter intent URL(与 Vue 原版一致)
|
|
3582
3636
|
const tweetLink = `https://twitter.com/intent/tweet?in_reply_to=${tweetId}`;
|
|
3583
3637
|
// 构造 title_express:Reply this tweet & tag 3 friends on Twitter
|
|
3584
3638
|
let titleExpress;
|
|
3585
3639
|
if (tagEnabled) {
|
|
3586
|
-
|
|
3640
|
+
// 与 Vue 原版保持一致:文案固定为 "tag 3 friends"
|
|
3641
|
+
// 不读取 friends_count,避免与原版展示不一致
|
|
3642
|
+
titleExpress = `Reply {this tweet}[link=${tweetLink}] & tag 3 friends on Twitter`;
|
|
3587
3643
|
}
|
|
3588
3644
|
else {
|
|
3589
3645
|
titleExpress = `Reply {this tweet}[link=${tweetLink}]`;
|
|
@@ -3605,7 +3661,6 @@ function normalizeReplyTweetAndHashTag(task) {
|
|
|
3605
3661
|
const tweetId = params.tweet_id || "";
|
|
3606
3662
|
const hashTag = params.hash_tag || "";
|
|
3607
3663
|
const tagEnabled = params.tag_enabled;
|
|
3608
|
-
const friendsCount = params.friends_count || 3;
|
|
3609
3664
|
// 格式化 hashtag 显示(逗号分隔 -> #tag1 #tag2)
|
|
3610
3665
|
const hashtagDisplay = hashTag
|
|
3611
3666
|
.split(",")
|
|
@@ -3618,10 +3673,12 @@ function normalizeReplyTweetAndHashTag(task) {
|
|
|
3618
3673
|
// 构造 title_express:Reply this tweet & tag 3 friends & include "#xxx #yyy" on Twitter
|
|
3619
3674
|
let titleExpress;
|
|
3620
3675
|
if (tagEnabled && hashtagDisplay) {
|
|
3621
|
-
|
|
3676
|
+
// 与 Vue 原版保持一致:文案固定为 "tag 3 friends"
|
|
3677
|
+
// 不读取 friends_count,避免与原版展示不一致
|
|
3678
|
+
titleExpress = `Reply {this tweet}[link=${tweetLink}] & tag 3 friends & include "${hashtagDisplay}" on Twitter`;
|
|
3622
3679
|
}
|
|
3623
3680
|
else if (tagEnabled) {
|
|
3624
|
-
titleExpress = `Reply {this tweet}[link=${tweetLink}] & tag
|
|
3681
|
+
titleExpress = `Reply {this tweet}[link=${tweetLink}] & tag 3 friends on Twitter`;
|
|
3625
3682
|
}
|
|
3626
3683
|
else if (hashtagDisplay) {
|
|
3627
3684
|
titleExpress = `Reply {this tweet}[link=${tweetLink}] & include "${hashtagDisplay}" on Twitter`;
|
|
@@ -3734,7 +3791,6 @@ function normalizeJoinTelegram(task) {
|
|
|
3734
3791
|
action_label: "Join",
|
|
3735
3792
|
action_link: inviteLink,
|
|
3736
3793
|
desc: desc || undefined,
|
|
3737
|
-
is_rich_text: true,
|
|
3738
3794
|
user_identity_type: UserIdentityType.Telegram,
|
|
3739
3795
|
};
|
|
3740
3796
|
}
|
|
@@ -3778,7 +3834,7 @@ const OUTER_API_SNS_MAP = {
|
|
|
3778
3834
|
* 与 CommunityTask normalizer 保持一致:
|
|
3779
3835
|
* - 有 target_url 时,title 为可点击链接(title_express),按钮文案为 "Start"
|
|
3780
3836
|
* - 无 target_url 时,使用 custom_title 纯文本
|
|
3781
|
-
* -
|
|
3837
|
+
* - 描述由 widget 层统一按富文本方式渲染
|
|
3782
3838
|
* - 根据 account_type 设置 user_identity_type / network
|
|
3783
3839
|
*/
|
|
3784
3840
|
function normalizeOuterAPI(task) {
|
|
@@ -3786,7 +3842,6 @@ function normalizeOuterAPI(task) {
|
|
|
3786
3842
|
return {
|
|
3787
3843
|
// 描述(富文本)
|
|
3788
3844
|
desc: params.desc || undefined,
|
|
3789
|
-
is_rich_text: true,
|
|
3790
3845
|
// 图标
|
|
3791
3846
|
icon: params.icon || undefined,
|
|
3792
3847
|
// 标题:有 target_url 时使用 title_express 让标题可点击
|
|
@@ -4462,23 +4517,9 @@ function createUserCenterApi(client) {
|
|
|
4462
4517
|
});
|
|
4463
4518
|
return res.result;
|
|
4464
4519
|
},
|
|
4465
|
-
async
|
|
4520
|
+
async getTokenWithdrawByNonce(params) {
|
|
4466
4521
|
const res = await client.request({
|
|
4467
|
-
url: "/v1/
|
|
4468
|
-
data: params,
|
|
4469
|
-
});
|
|
4470
|
-
return res.result;
|
|
4471
|
-
},
|
|
4472
|
-
async getGasStationAirdropResult(params) {
|
|
4473
|
-
const res = await client.request({
|
|
4474
|
-
url: "/v1/getGasStationAirdropResult",
|
|
4475
|
-
data: params,
|
|
4476
|
-
});
|
|
4477
|
-
return res.result;
|
|
4478
|
-
},
|
|
4479
|
-
async getTokenWithdrawGasFreeTimes(params) {
|
|
4480
|
-
const res = await client.request({
|
|
4481
|
-
url: "/v1/getTokenWithdrawGasFreeTimes",
|
|
4522
|
+
url: "/v1/getTokenWithdrawByNonce",
|
|
4482
4523
|
data: params,
|
|
4483
4524
|
});
|
|
4484
4525
|
return res.result;
|
|
@@ -4590,20 +4631,6 @@ var LockedType;
|
|
|
4590
4631
|
/** 社区里程碑锁定 */
|
|
4591
4632
|
LockedType["CommunityMilestone"] = "CommunityMilestone";
|
|
4592
4633
|
})(LockedType || (LockedType = {}));
|
|
4593
|
-
// ============================================================================
|
|
4594
|
-
// Gas Station(免 Gas 提现)类型
|
|
4595
|
-
// ============================================================================
|
|
4596
|
-
/**
|
|
4597
|
-
* Gas Station 请求状态
|
|
4598
|
-
*/
|
|
4599
|
-
var GasStationRequestStatus;
|
|
4600
|
-
(function (GasStationRequestStatus) {
|
|
4601
|
-
GasStationRequestStatus["All"] = "All";
|
|
4602
|
-
GasStationRequestStatus["Pending"] = "Pending";
|
|
4603
|
-
GasStationRequestStatus["Processing"] = "Processing";
|
|
4604
|
-
GasStationRequestStatus["Success"] = "Success";
|
|
4605
|
-
GasStationRequestStatus["Failed"] = "Failed";
|
|
4606
|
-
})(GasStationRequestStatus || (GasStationRequestStatus = {}));
|
|
4607
4634
|
/**
|
|
4608
4635
|
* 奖励分发方式
|
|
4609
4636
|
*/
|
|
@@ -4992,12 +5019,13 @@ function createNftClaimApi(client) {
|
|
|
4992
5019
|
});
|
|
4993
5020
|
return res.result;
|
|
4994
5021
|
},
|
|
4995
|
-
// ===========
|
|
4996
|
-
async
|
|
4997
|
-
await client.request({
|
|
4998
|
-
url: "/v1/
|
|
5022
|
+
// =========== Standard NFT ===========
|
|
5023
|
+
async nftWithdraw(params) {
|
|
5024
|
+
const res = await client.request({
|
|
5025
|
+
url: "/v1/nftWithdraw",
|
|
4999
5026
|
data: params,
|
|
5000
5027
|
});
|
|
5028
|
+
return res.result;
|
|
5001
5029
|
},
|
|
5002
5030
|
};
|
|
5003
5031
|
}
|
|
@@ -5039,8 +5067,6 @@ var NftClaimErrorType;
|
|
|
5039
5067
|
// ========== API 相关 ==========
|
|
5040
5068
|
/** 获取签名失败 */
|
|
5041
5069
|
NftClaimErrorType["SignatureError"] = "SIGNATURE_ERROR";
|
|
5042
|
-
/** Gas Station 次数不足 */
|
|
5043
|
-
NftClaimErrorType["GasStationNotEnough"] = "GAS_STATION_NOT_ENOUGH";
|
|
5044
5070
|
/** API 请求失败 */
|
|
5045
5071
|
NftClaimErrorType["ApiError"] = "API_ERROR";
|
|
5046
5072
|
// ========== 数据相关 ==========
|
|
@@ -5054,6 +5080,28 @@ var NftClaimErrorType;
|
|
|
5054
5080
|
/** 未知错误 */
|
|
5055
5081
|
NftClaimErrorType["Unknown"] = "UNKNOWN";
|
|
5056
5082
|
})(NftClaimErrorType || (NftClaimErrorType = {}));
|
|
5083
|
+
/**
|
|
5084
|
+
* 判断 unknown 是否是对象 Record
|
|
5085
|
+
*/
|
|
5086
|
+
function isRecord(value) {
|
|
5087
|
+
return typeof value === "object" && value !== null;
|
|
5088
|
+
}
|
|
5089
|
+
/**
|
|
5090
|
+
* 规范化错误字符串。
|
|
5091
|
+
*
|
|
5092
|
+
* 说明:
|
|
5093
|
+
* - 过滤空字符串和 "[object Object]",避免 UI 直接显示不可读文案。
|
|
5094
|
+
*/
|
|
5095
|
+
function normalizeErrorText(value) {
|
|
5096
|
+
if (typeof value !== "string") {
|
|
5097
|
+
return null;
|
|
5098
|
+
}
|
|
5099
|
+
const trimmed = value.trim();
|
|
5100
|
+
if (!trimmed || trimmed === "[object Object]") {
|
|
5101
|
+
return null;
|
|
5102
|
+
}
|
|
5103
|
+
return trimmed;
|
|
5104
|
+
}
|
|
5057
5105
|
// ============================================================================
|
|
5058
5106
|
// 错误类
|
|
5059
5107
|
// ============================================================================
|
|
@@ -5088,21 +5136,29 @@ class NftClaimError extends Error {
|
|
|
5088
5136
|
if (error instanceof NftClaimError) {
|
|
5089
5137
|
return error;
|
|
5090
5138
|
}
|
|
5091
|
-
const message =
|
|
5139
|
+
const message = NftClaimError.extractErrorMessage(error);
|
|
5092
5140
|
const cause = error instanceof Error ? error : undefined;
|
|
5093
5141
|
// 尝试识别常见错误类型
|
|
5094
|
-
const type = NftClaimError.inferErrorType(message);
|
|
5095
|
-
|
|
5142
|
+
const type = NftClaimError.inferErrorType(message, error);
|
|
5143
|
+
// 如果解析不出可读文案,则回退到类型默认文案。
|
|
5144
|
+
const fallbackMessage = DEFAULT_ERROR_MESSAGES[type] ?? DEFAULT_ERROR_MESSAGES[NftClaimErrorType.Unknown];
|
|
5145
|
+
const finalMessage = normalizeErrorText(message) ?? fallbackMessage;
|
|
5146
|
+
return new NftClaimError(type, finalMessage, cause);
|
|
5096
5147
|
}
|
|
5097
5148
|
/**
|
|
5098
5149
|
* 根据错误消息推断错误类型
|
|
5099
5150
|
*/
|
|
5100
|
-
static inferErrorType(message) {
|
|
5151
|
+
static inferErrorType(message, error) {
|
|
5101
5152
|
const lowerMessage = message.toLowerCase();
|
|
5102
5153
|
// 用户拒绝
|
|
5103
|
-
if (
|
|
5154
|
+
if (NftClaimError.hasUserRejectedCode(error) ||
|
|
5155
|
+
lowerMessage.includes("user rejected") ||
|
|
5104
5156
|
lowerMessage.includes("user denied") ||
|
|
5105
|
-
lowerMessage.includes("
|
|
5157
|
+
lowerMessage.includes("rejected the request") ||
|
|
5158
|
+
lowerMessage.includes("request rejected") ||
|
|
5159
|
+
lowerMessage.includes("denied transaction") ||
|
|
5160
|
+
lowerMessage.includes("cancelled") ||
|
|
5161
|
+
lowerMessage.includes("canceled")) {
|
|
5106
5162
|
return NftClaimErrorType.WalletRejected;
|
|
5107
5163
|
}
|
|
5108
5164
|
// Gas 不足
|
|
@@ -5117,6 +5173,122 @@ class NftClaimError extends Error {
|
|
|
5117
5173
|
}
|
|
5118
5174
|
return NftClaimErrorType.Unknown;
|
|
5119
5175
|
}
|
|
5176
|
+
/**
|
|
5177
|
+
* 从 unknown 错误中尽量提取可读 message,避免落到 "[object Object]"。
|
|
5178
|
+
*/
|
|
5179
|
+
static extractErrorMessage(error) {
|
|
5180
|
+
const visited = new WeakSet();
|
|
5181
|
+
const walk = (value) => {
|
|
5182
|
+
const directText = normalizeErrorText(value);
|
|
5183
|
+
if (directText) {
|
|
5184
|
+
return directText;
|
|
5185
|
+
}
|
|
5186
|
+
if (Array.isArray(value)) {
|
|
5187
|
+
for (const item of value) {
|
|
5188
|
+
const nestedText = walk(item);
|
|
5189
|
+
if (nestedText) {
|
|
5190
|
+
return nestedText;
|
|
5191
|
+
}
|
|
5192
|
+
}
|
|
5193
|
+
return null;
|
|
5194
|
+
}
|
|
5195
|
+
if (value instanceof Error) {
|
|
5196
|
+
const messageText = normalizeErrorText(value.message);
|
|
5197
|
+
if (messageText) {
|
|
5198
|
+
return messageText;
|
|
5199
|
+
}
|
|
5200
|
+
const errorWithCause = value;
|
|
5201
|
+
if ("cause" in errorWithCause) {
|
|
5202
|
+
const causeText = walk(errorWithCause.cause);
|
|
5203
|
+
if (causeText) {
|
|
5204
|
+
return causeText;
|
|
5205
|
+
}
|
|
5206
|
+
}
|
|
5207
|
+
}
|
|
5208
|
+
if (!isRecord(value)) {
|
|
5209
|
+
return null;
|
|
5210
|
+
}
|
|
5211
|
+
if (visited.has(value)) {
|
|
5212
|
+
return null;
|
|
5213
|
+
}
|
|
5214
|
+
visited.add(value);
|
|
5215
|
+
// 常见钱包 / RPC 错误字段优先级
|
|
5216
|
+
const preferredMessageKeys = ["shortMessage", "reason", "details", "message"];
|
|
5217
|
+
for (const key of preferredMessageKeys) {
|
|
5218
|
+
const keyText = normalizeErrorText(value[key]);
|
|
5219
|
+
if (keyText) {
|
|
5220
|
+
return keyText;
|
|
5221
|
+
}
|
|
5222
|
+
}
|
|
5223
|
+
// 常见嵌套错误字段
|
|
5224
|
+
const nestedKeys = [
|
|
5225
|
+
"cause",
|
|
5226
|
+
"error",
|
|
5227
|
+
"data",
|
|
5228
|
+
"originalError",
|
|
5229
|
+
"innerError",
|
|
5230
|
+
"response",
|
|
5231
|
+
];
|
|
5232
|
+
for (const key of nestedKeys) {
|
|
5233
|
+
const nestedText = walk(value[key]);
|
|
5234
|
+
if (nestedText) {
|
|
5235
|
+
return nestedText;
|
|
5236
|
+
}
|
|
5237
|
+
}
|
|
5238
|
+
return null;
|
|
5239
|
+
};
|
|
5240
|
+
return walk(error) ?? "";
|
|
5241
|
+
}
|
|
5242
|
+
/**
|
|
5243
|
+
* 通过常见错误码识别是否为用户主动取消。
|
|
5244
|
+
*/
|
|
5245
|
+
static hasUserRejectedCode(error) {
|
|
5246
|
+
const visited = new WeakSet();
|
|
5247
|
+
const walk = (value) => {
|
|
5248
|
+
if (Array.isArray(value)) {
|
|
5249
|
+
return value.some((item) => walk(item));
|
|
5250
|
+
}
|
|
5251
|
+
if (!isRecord(value)) {
|
|
5252
|
+
return false;
|
|
5253
|
+
}
|
|
5254
|
+
if (visited.has(value)) {
|
|
5255
|
+
return false;
|
|
5256
|
+
}
|
|
5257
|
+
visited.add(value);
|
|
5258
|
+
const errorCode = value.code;
|
|
5259
|
+
if (typeof errorCode === "number" && errorCode === 4001) {
|
|
5260
|
+
return true;
|
|
5261
|
+
}
|
|
5262
|
+
if (typeof errorCode === "string") {
|
|
5263
|
+
const normalizedCode = errorCode.toUpperCase();
|
|
5264
|
+
if (normalizedCode.includes("USER_REJECT") ||
|
|
5265
|
+
normalizedCode.includes("ACTION_REJECTED") ||
|
|
5266
|
+
normalizedCode.includes("REJECTED_REQUEST")) {
|
|
5267
|
+
return true;
|
|
5268
|
+
}
|
|
5269
|
+
}
|
|
5270
|
+
const errorName = value.name;
|
|
5271
|
+
if (typeof errorName === "string" &&
|
|
5272
|
+
errorName.toLowerCase().includes("userrejected")) {
|
|
5273
|
+
return true;
|
|
5274
|
+
}
|
|
5275
|
+
const nestedKeys = [
|
|
5276
|
+
"cause",
|
|
5277
|
+
"error",
|
|
5278
|
+
"data",
|
|
5279
|
+
"originalError",
|
|
5280
|
+
"innerError",
|
|
5281
|
+
"response",
|
|
5282
|
+
];
|
|
5283
|
+
for (const key of nestedKeys) {
|
|
5284
|
+
if (walk(value[key])) {
|
|
5285
|
+
return true;
|
|
5286
|
+
}
|
|
5287
|
+
}
|
|
5288
|
+
return false;
|
|
5289
|
+
};
|
|
5290
|
+
return walk(error);
|
|
5291
|
+
}
|
|
5120
5292
|
/**
|
|
5121
5293
|
* 判断是否是用户主动取消
|
|
5122
5294
|
*/
|
|
@@ -5153,7 +5325,6 @@ const DEFAULT_ERROR_MESSAGES = {
|
|
|
5153
5325
|
[NftClaimErrorType.TransactionFailed]: "Transaction failed.",
|
|
5154
5326
|
[NftClaimErrorType.InsufficientGas]: "Insufficient gas. Please add more funds to your wallet.",
|
|
5155
5327
|
[NftClaimErrorType.SignatureError]: "Failed to get signature from server.",
|
|
5156
|
-
[NftClaimErrorType.GasStationNotEnough]: "Gas station quota exceeded. Please try again later.",
|
|
5157
5328
|
[NftClaimErrorType.ApiError]: "API request failed. Please try again.",
|
|
5158
5329
|
[NftClaimErrorType.MissingRewardId]: "Missing reward information.",
|
|
5159
5330
|
[NftClaimErrorType.NotClaimable]: "This NFT is not claimable.",
|
|
@@ -5233,6 +5404,239 @@ class NftTransaction {
|
|
|
5233
5404
|
}
|
|
5234
5405
|
}
|
|
5235
5406
|
|
|
5407
|
+
var nftWithdrawAbi = [
|
|
5408
|
+
{
|
|
5409
|
+
inputs: [
|
|
5410
|
+
{
|
|
5411
|
+
internalType: "uint256",
|
|
5412
|
+
name: "userId",
|
|
5413
|
+
type: "uint256"
|
|
5414
|
+
},
|
|
5415
|
+
{
|
|
5416
|
+
internalType: "enum TaskonFundNFT.NFTType",
|
|
5417
|
+
name: "ty",
|
|
5418
|
+
type: "uint8"
|
|
5419
|
+
},
|
|
5420
|
+
{
|
|
5421
|
+
internalType: "address",
|
|
5422
|
+
name: "nftCollection",
|
|
5423
|
+
type: "address"
|
|
5424
|
+
},
|
|
5425
|
+
{
|
|
5426
|
+
components: [
|
|
5427
|
+
{
|
|
5428
|
+
internalType: "uint256",
|
|
5429
|
+
name: "tokenId",
|
|
5430
|
+
type: "uint256"
|
|
5431
|
+
},
|
|
5432
|
+
{
|
|
5433
|
+
internalType: "uint256",
|
|
5434
|
+
name: "amount",
|
|
5435
|
+
type: "uint256"
|
|
5436
|
+
}
|
|
5437
|
+
],
|
|
5438
|
+
internalType: "struct TaskonFundNFT.WithdrawTokenAmount[]",
|
|
5439
|
+
name: "tokenAmounts",
|
|
5440
|
+
type: "tuple[]"
|
|
5441
|
+
},
|
|
5442
|
+
{
|
|
5443
|
+
internalType: "address",
|
|
5444
|
+
name: "receiver",
|
|
5445
|
+
type: "address"
|
|
5446
|
+
},
|
|
5447
|
+
{
|
|
5448
|
+
internalType: "uint256",
|
|
5449
|
+
name: "nonce",
|
|
5450
|
+
type: "uint256"
|
|
5451
|
+
},
|
|
5452
|
+
{
|
|
5453
|
+
internalType: "uint256",
|
|
5454
|
+
name: "expiredHeight",
|
|
5455
|
+
type: "uint256"
|
|
5456
|
+
},
|
|
5457
|
+
{
|
|
5458
|
+
internalType: "bytes[]",
|
|
5459
|
+
name: "sigs",
|
|
5460
|
+
type: "bytes[]"
|
|
5461
|
+
}
|
|
5462
|
+
],
|
|
5463
|
+
name: "withdraw",
|
|
5464
|
+
outputs: [
|
|
5465
|
+
],
|
|
5466
|
+
stateMutability: "nonpayable",
|
|
5467
|
+
type: "function"
|
|
5468
|
+
}
|
|
5469
|
+
];
|
|
5470
|
+
|
|
5471
|
+
/**
|
|
5472
|
+
* Withdraw Standard NFT 交易类
|
|
5473
|
+
*
|
|
5474
|
+
* 用于领取普通 NFT 奖励(RewardType.Nft)
|
|
5475
|
+
* 逻辑与主站 WithdrawNft 保持一致:
|
|
5476
|
+
* 1. 调 /v1/nftWithdraw 获取签名参数
|
|
5477
|
+
* 2. 调 TaskOnFundNFT 合约 withdraw 方法
|
|
5478
|
+
*/
|
|
5479
|
+
/**
|
|
5480
|
+
* 获取 NFT 类型枚举值(与主站保持一致)
|
|
5481
|
+
* - ERC721 => 0
|
|
5482
|
+
* - ERC1155 => 1
|
|
5483
|
+
*/
|
|
5484
|
+
function getNftTypeValue(nftType) {
|
|
5485
|
+
return nftType === "ERC1155" ? 1 : 0;
|
|
5486
|
+
}
|
|
5487
|
+
/**
|
|
5488
|
+
* 将 token 数组转换成合约 withdraw 参数格式
|
|
5489
|
+
*/
|
|
5490
|
+
function toWithdrawTokenAmounts(tokens) {
|
|
5491
|
+
return tokens.map((item) => ({
|
|
5492
|
+
tokenId: BigInt(item.tokenId),
|
|
5493
|
+
amount: BigInt(item.amount),
|
|
5494
|
+
}));
|
|
5495
|
+
}
|
|
5496
|
+
/**
|
|
5497
|
+
* 简易 ABI 编码器
|
|
5498
|
+
*
|
|
5499
|
+
* 由于 core 模块保持框架无关,不直接依赖 viem/ethers 编码。
|
|
5500
|
+
* 这里返回结构化数据供上层钱包实现处理。
|
|
5501
|
+
*/
|
|
5502
|
+
function encodeWithdrawData(params) {
|
|
5503
|
+
return JSON.stringify({
|
|
5504
|
+
method: "withdraw",
|
|
5505
|
+
params: {
|
|
5506
|
+
userId: params.userId,
|
|
5507
|
+
nftType: getNftTypeValue(params.nftType),
|
|
5508
|
+
nftContract: params.nftContract,
|
|
5509
|
+
tokens: params.tokens,
|
|
5510
|
+
receiverAddress: params.receiverAddress,
|
|
5511
|
+
nonce: params.nonce,
|
|
5512
|
+
expiredHeight: params.expiredHeight,
|
|
5513
|
+
signatures: params.signatures,
|
|
5514
|
+
},
|
|
5515
|
+
abi: nftWithdrawAbi,
|
|
5516
|
+
});
|
|
5517
|
+
}
|
|
5518
|
+
/**
|
|
5519
|
+
* Standard NFT Withdraw 交易类
|
|
5520
|
+
*/
|
|
5521
|
+
class WithdrawNft extends NftTransaction {
|
|
5522
|
+
/** API 实例 */
|
|
5523
|
+
api;
|
|
5524
|
+
/** 用户 ID */
|
|
5525
|
+
userId;
|
|
5526
|
+
/** 链名称 */
|
|
5527
|
+
chainName;
|
|
5528
|
+
/** target id(campaign/event id) */
|
|
5529
|
+
targetId;
|
|
5530
|
+
/** target type */
|
|
5531
|
+
targetType;
|
|
5532
|
+
/** 奖励 ID */
|
|
5533
|
+
rewardId;
|
|
5534
|
+
/** TaskOnFund NFT 合约地址 */
|
|
5535
|
+
taskonfundNftContract;
|
|
5536
|
+
/** NFT collection id */
|
|
5537
|
+
collectionId;
|
|
5538
|
+
/** NFT 合约地址 */
|
|
5539
|
+
nftContract;
|
|
5540
|
+
/** NFT 类型 */
|
|
5541
|
+
nftType;
|
|
5542
|
+
/** token 列表 */
|
|
5543
|
+
tokens;
|
|
5544
|
+
/** 重发接收地址 */
|
|
5545
|
+
receiveAddress;
|
|
5546
|
+
/** 重发 nonce */
|
|
5547
|
+
nonce;
|
|
5548
|
+
constructor(params) {
|
|
5549
|
+
super(params);
|
|
5550
|
+
this.api = params.api;
|
|
5551
|
+
this.userId = params.userId;
|
|
5552
|
+
this.chainName = params.chainName;
|
|
5553
|
+
this.targetId = params.targetId;
|
|
5554
|
+
this.targetType = params.targetType;
|
|
5555
|
+
this.rewardId = params.rewardId;
|
|
5556
|
+
this.taskonfundNftContract = params.taskonfundNftContract;
|
|
5557
|
+
this.collectionId = params.collectionId;
|
|
5558
|
+
this.nftContract = params.nftContract;
|
|
5559
|
+
this.nftType = params.nftType;
|
|
5560
|
+
this.tokens = params.tokens;
|
|
5561
|
+
this.receiveAddress = params.receiveAddress;
|
|
5562
|
+
this.nonce = params.nonce;
|
|
5563
|
+
}
|
|
5564
|
+
/**
|
|
5565
|
+
* 执行 Standard NFT withdraw
|
|
5566
|
+
*/
|
|
5567
|
+
async withdraw() {
|
|
5568
|
+
await this.checkAccountAndNetwork();
|
|
5569
|
+
const isResend = Boolean(this.receiveAddress && this.nonce);
|
|
5570
|
+
const receiverAddress = isResend
|
|
5571
|
+
? this.receiveAddress
|
|
5572
|
+
: this.userAddress;
|
|
5573
|
+
const sigRes = await this.api.nftWithdraw({
|
|
5574
|
+
chain: this.chainName,
|
|
5575
|
+
collection_id: this.collectionId,
|
|
5576
|
+
receiver_address: receiverAddress,
|
|
5577
|
+
token_ids: this.tokens.map((item) => ({
|
|
5578
|
+
token_id: item.tokenId,
|
|
5579
|
+
quantity: item.amount,
|
|
5580
|
+
})),
|
|
5581
|
+
...(this.targetType === "campaign" || this.targetType === "event"
|
|
5582
|
+
? {
|
|
5583
|
+
campaign_id: this.targetId,
|
|
5584
|
+
reward_id: this.rewardId,
|
|
5585
|
+
}
|
|
5586
|
+
: undefined),
|
|
5587
|
+
...(isResend
|
|
5588
|
+
? {
|
|
5589
|
+
nonce: this.nonce,
|
|
5590
|
+
}
|
|
5591
|
+
: undefined),
|
|
5592
|
+
});
|
|
5593
|
+
return this.invokeWithdraw(sigRes, receiverAddress);
|
|
5594
|
+
}
|
|
5595
|
+
/**
|
|
5596
|
+
* 与 NftTransaction 接口保持一致
|
|
5597
|
+
*/
|
|
5598
|
+
async claim() {
|
|
5599
|
+
return this.withdraw();
|
|
5600
|
+
}
|
|
5601
|
+
/**
|
|
5602
|
+
* 获取合约调用参数(供外部编码库使用)
|
|
5603
|
+
*/
|
|
5604
|
+
getContractCallParams(withdrawResult) {
|
|
5605
|
+
const receiverAddress = this.receiveAddress ?? this.userAddress;
|
|
5606
|
+
return {
|
|
5607
|
+
contractAddress: this.taskonfundNftContract,
|
|
5608
|
+
methodName: "withdraw",
|
|
5609
|
+
args: [
|
|
5610
|
+
BigInt(this.userId),
|
|
5611
|
+
getNftTypeValue(this.nftType),
|
|
5612
|
+
this.nftContract,
|
|
5613
|
+
toWithdrawTokenAmounts(this.tokens),
|
|
5614
|
+
receiverAddress,
|
|
5615
|
+
BigInt(withdrawResult.nonce),
|
|
5616
|
+
BigInt(withdrawResult.expired_height),
|
|
5617
|
+
withdrawResult.signatures,
|
|
5618
|
+
],
|
|
5619
|
+
abi: nftWithdrawAbi,
|
|
5620
|
+
};
|
|
5621
|
+
}
|
|
5622
|
+
/**
|
|
5623
|
+
* 执行链上 withdraw 调用
|
|
5624
|
+
*/
|
|
5625
|
+
async invokeWithdraw(withdrawResult, receiverAddress) {
|
|
5626
|
+
const data = encodeWithdrawData({
|
|
5627
|
+
userId: this.userId,
|
|
5628
|
+
nftType: this.nftType,
|
|
5629
|
+
nftContract: this.nftContract,
|
|
5630
|
+
tokens: this.tokens,
|
|
5631
|
+
receiverAddress,
|
|
5632
|
+
nonce: withdrawResult.nonce,
|
|
5633
|
+
expiredHeight: withdrawResult.expired_height,
|
|
5634
|
+
signatures: withdrawResult.signatures,
|
|
5635
|
+
});
|
|
5636
|
+
return this.sendTransaction(this.taskonfundNftContract, data);
|
|
5637
|
+
}
|
|
5638
|
+
}
|
|
5639
|
+
|
|
5236
5640
|
var bMintedNftAbi = [
|
|
5237
5641
|
{
|
|
5238
5642
|
inputs: [
|
|
@@ -5568,6 +5972,7 @@ function encodeMintData(account, cid, tokenUri, limit, unsigned, signature) {
|
|
|
5568
5972
|
* chainId: 1,
|
|
5569
5973
|
* userAddress: '0x...',
|
|
5570
5974
|
* sigRes: apiResponse,
|
|
5975
|
+
* contractAddress: chain.contract,
|
|
5571
5976
|
* });
|
|
5572
5977
|
*
|
|
5573
5978
|
* const txHash = await tx.claim();
|
|
@@ -5576,9 +5981,12 @@ function encodeMintData(account, cid, tokenUri, limit, unsigned, signature) {
|
|
|
5576
5981
|
class ClaimCapNft extends NftTransaction {
|
|
5577
5982
|
/** API 返回的签名响应 */
|
|
5578
5983
|
sigRes;
|
|
5984
|
+
/** CAP NFT 合约地址(对齐主站 chain.contract) */
|
|
5985
|
+
contractAddress;
|
|
5579
5986
|
constructor(params) {
|
|
5580
5987
|
super(params);
|
|
5581
5988
|
this.sigRes = params.sigRes;
|
|
5989
|
+
this.contractAddress = params.contractAddress;
|
|
5582
5990
|
}
|
|
5583
5991
|
/**
|
|
5584
5992
|
* 执行 Claim 操作
|
|
@@ -5595,7 +6003,7 @@ class ClaimCapNft extends NftTransaction {
|
|
|
5595
6003
|
// 2. 编码合约调用数据
|
|
5596
6004
|
const data = encodeMintData(this.userAddress, BigInt(this.sigRes.campaign_id), this.sigRes.token_uri, BigInt(this.sigRes.total), this.sigRes.hash, this.sigRes.signature);
|
|
5597
6005
|
// 3. 调用合约
|
|
5598
|
-
return this.sendTransaction(this.
|
|
6006
|
+
return this.sendTransaction(this.contractAddress, data);
|
|
5599
6007
|
}
|
|
5600
6008
|
/**
|
|
5601
6009
|
* 获取合约调用参数(用于外部编码)
|
|
@@ -5604,7 +6012,7 @@ class ClaimCapNft extends NftTransaction {
|
|
|
5604
6012
|
*/
|
|
5605
6013
|
getContractCallParams() {
|
|
5606
6014
|
return {
|
|
5607
|
-
contractAddress: this.
|
|
6015
|
+
contractAddress: this.contractAddress,
|
|
5608
6016
|
methodName: "mint",
|
|
5609
6017
|
args: [
|
|
5610
6018
|
this.userAddress,
|
|
@@ -5636,17 +6044,15 @@ class ClaimCapNft extends NftTransaction {
|
|
|
5636
6044
|
* }
|
|
5637
6045
|
* }
|
|
5638
6046
|
*
|
|
5639
|
-
* pendingKey
|
|
5640
|
-
* -
|
|
5641
|
-
* -
|
|
6047
|
+
* pendingKey 格式(与主站保持一致):
|
|
6048
|
+
* - Campaign: `campaign-${targetId}-${rewardId}`
|
|
6049
|
+
* - Event: `event-${targetId}-${rewardId}`
|
|
5642
6050
|
*/
|
|
5643
6051
|
// ============================================================================
|
|
5644
6052
|
// 常量定义
|
|
5645
6053
|
// ============================================================================
|
|
5646
6054
|
/** localStorage 存储 key */
|
|
5647
6055
|
const STORAGE_KEY = "taskon_nft_pending";
|
|
5648
|
-
/** Pending 记录过期时间(24 小时) */
|
|
5649
|
-
const PENDING_EXPIRE_TIME = 24 * 60 * 60 * 1000;
|
|
5650
6056
|
// ============================================================================
|
|
5651
6057
|
// 内部辅助函数
|
|
5652
6058
|
// ============================================================================
|
|
@@ -5689,13 +6095,13 @@ function savePendingHashMap(map) {
|
|
|
5689
6095
|
/**
|
|
5690
6096
|
* 生成 Pending Key
|
|
5691
6097
|
*
|
|
5692
|
-
* @param
|
|
6098
|
+
* @param targetType - claim 目标类型(campaign / event / 其他业务前缀)
|
|
5693
6099
|
* @param campaignId - Campaign ID
|
|
5694
6100
|
* @param rewardId - 奖励 ID
|
|
5695
6101
|
* @returns 格式化的 pending key
|
|
5696
6102
|
*/
|
|
5697
|
-
function generatePendingKey(
|
|
5698
|
-
return `${
|
|
6103
|
+
function generatePendingKey(targetType, campaignId, rewardId) {
|
|
6104
|
+
return `${targetType}-${campaignId}-${rewardId}`;
|
|
5699
6105
|
}
|
|
5700
6106
|
/**
|
|
5701
6107
|
* 设置 Pending Hash
|
|
@@ -5724,7 +6130,7 @@ function setPendingHash(userId, pendingKey, hash, chainName) {
|
|
|
5724
6130
|
*
|
|
5725
6131
|
* @param userId - 用户 ID
|
|
5726
6132
|
* @param pendingKey - Pending Key
|
|
5727
|
-
* @returns Pending
|
|
6133
|
+
* @returns Pending 信息,如果不存在则返回 null
|
|
5728
6134
|
*/
|
|
5729
6135
|
function getPendingHash(userId, pendingKey) {
|
|
5730
6136
|
const map = getPendingHashMap();
|
|
@@ -5732,12 +6138,8 @@ function getPendingHash(userId, pendingKey) {
|
|
|
5732
6138
|
if (!pending) {
|
|
5733
6139
|
return null;
|
|
5734
6140
|
}
|
|
5735
|
-
//
|
|
5736
|
-
|
|
5737
|
-
// 清除过期的 pending
|
|
5738
|
-
clearPendingHash(userId, pendingKey);
|
|
5739
|
-
return null;
|
|
5740
|
-
}
|
|
6141
|
+
// 与原版 Vue 行为保持一致:
|
|
6142
|
+
// pending 记录不做自动过期清理,仅在业务显式 clear 时移除。
|
|
5741
6143
|
return pending;
|
|
5742
6144
|
}
|
|
5743
6145
|
/**
|
|
@@ -5765,15 +6167,9 @@ function clearPendingHash(userId, pendingKey) {
|
|
|
5765
6167
|
*/
|
|
5766
6168
|
function getUserPendingHashes(userId) {
|
|
5767
6169
|
const map = getPendingHashMap();
|
|
5768
|
-
|
|
5769
|
-
|
|
5770
|
-
|
|
5771
|
-
for (const [key, pending] of Object.entries(userPending)) {
|
|
5772
|
-
if (Date.now() - pending.timestamp <= PENDING_EXPIRE_TIME) {
|
|
5773
|
-
result[key] = pending;
|
|
5774
|
-
}
|
|
5775
|
-
}
|
|
5776
|
-
return result;
|
|
6170
|
+
// 返回浅拷贝,避免调用方直接改写内部引用对象。
|
|
6171
|
+
// 这里不做任何 TTL 过滤,策略与原版保持一致。
|
|
6172
|
+
return { ...(map[userId] || {}) };
|
|
5777
6173
|
}
|
|
5778
6174
|
/**
|
|
5779
6175
|
* 清除用户所有的 Pending Hash
|
|
@@ -5789,45 +6185,21 @@ function clearAllUserPendingHashes(userId) {
|
|
|
5789
6185
|
}
|
|
5790
6186
|
/**
|
|
5791
6187
|
* 清除所有过期的 Pending Hash
|
|
5792
|
-
*
|
|
6188
|
+
*
|
|
6189
|
+
* 为兼容历史 API 而保留:
|
|
6190
|
+
* 当前策略已改为与原版一致(无自动过期),因此该函数不执行任何操作。
|
|
5793
6191
|
*/
|
|
5794
6192
|
function cleanupExpiredPendingHashes() {
|
|
5795
|
-
|
|
5796
|
-
|
|
5797
|
-
|
|
5798
|
-
const numUserId = Number(userId);
|
|
5799
|
-
const userMap = map[numUserId];
|
|
5800
|
-
// 跳过不存在的用户映射
|
|
5801
|
-
if (!userMap) {
|
|
5802
|
-
continue;
|
|
5803
|
-
}
|
|
5804
|
-
for (const pendingKey of Object.keys(userMap)) {
|
|
5805
|
-
const pending = userMap[pendingKey];
|
|
5806
|
-
// 跳过不存在的 pending
|
|
5807
|
-
if (!pending) {
|
|
5808
|
-
continue;
|
|
5809
|
-
}
|
|
5810
|
-
if (Date.now() - pending.timestamp > PENDING_EXPIRE_TIME) {
|
|
5811
|
-
delete userMap[pendingKey];
|
|
5812
|
-
hasChanges = true;
|
|
5813
|
-
}
|
|
5814
|
-
}
|
|
5815
|
-
// 如果用户没有其他 pending,清除用户条目
|
|
5816
|
-
if (Object.keys(userMap).length === 0) {
|
|
5817
|
-
delete map[numUserId];
|
|
5818
|
-
hasChanges = true;
|
|
5819
|
-
}
|
|
5820
|
-
}
|
|
5821
|
-
if (hasChanges) {
|
|
5822
|
-
savePendingHashMap(map);
|
|
5823
|
-
}
|
|
6193
|
+
// 兼容导出 API:
|
|
6194
|
+
// 旧版本存在基于 24h TTL 的自动清理逻辑;
|
|
6195
|
+
// 当前为对齐原版,此函数改为 no-op,保留仅用于避免破坏性升级。
|
|
5824
6196
|
}
|
|
5825
6197
|
|
|
5826
6198
|
/**
|
|
5827
6199
|
* NftClaim 模块类型定义
|
|
5828
6200
|
*
|
|
5829
6201
|
* 包含 NFT Claim 所需的所有类型定义
|
|
5830
|
-
* 支持 Minted NFT
|
|
6202
|
+
* 支持 Standard NFT / Minted NFT / CAP NFT 三种类型
|
|
5831
6203
|
*/
|
|
5832
6204
|
// ============================================================================
|
|
5833
6205
|
// NFT 类型枚举
|
|
@@ -5837,6 +6209,8 @@ function cleanupExpiredPendingHashes() {
|
|
|
5837
6209
|
*/
|
|
5838
6210
|
var NftClaimType;
|
|
5839
6211
|
(function (NftClaimType) {
|
|
6212
|
+
/** 标准 NFT(使用 TaskOnFundNFT withdraw 方法) */
|
|
6213
|
+
NftClaimType["Standard"] = "standard";
|
|
5840
6214
|
/** B 端铸造的 NFT(使用 claim 合约方法) */
|
|
5841
6215
|
NftClaimType["Minted"] = "minted";
|
|
5842
6216
|
/** TaskOn 平台 NFT(使用 mint 合约方法) */
|
|
@@ -5867,8 +6241,17 @@ function isMintedNftReward(value) {
|
|
|
5867
6241
|
* CAP NFT 特征:有 gas_covered_by 字段但没有 nft_address
|
|
5868
6242
|
*/
|
|
5869
6243
|
function isCapNftReward(value) {
|
|
5870
|
-
return
|
|
5871
|
-
|
|
6244
|
+
return "gas_covered_by" in value && !("nft_address" in value);
|
|
6245
|
+
}
|
|
6246
|
+
/**
|
|
6247
|
+
* 判断 NFT 奖励值是否是 Standard NFT 类型
|
|
6248
|
+
*
|
|
6249
|
+
* Standard NFT 特征:有 nft_address 且没有 gas_covered_by
|
|
6250
|
+
*/
|
|
6251
|
+
function isStandardNftReward(value) {
|
|
6252
|
+
return ("nft_address" in value &&
|
|
6253
|
+
!("gas_covered_by" in value) &&
|
|
6254
|
+
typeof value.nft_address === "string");
|
|
5872
6255
|
}
|
|
5873
6256
|
/**
|
|
5874
6257
|
* 获取 NFT Claim 类型
|
|
@@ -5880,7 +6263,10 @@ function getNftClaimType(value) {
|
|
|
5880
6263
|
if (isCapNftReward(value)) {
|
|
5881
6264
|
return NftClaimType.Cap;
|
|
5882
6265
|
}
|
|
5883
|
-
|
|
6266
|
+
if (isMintedNftReward(value)) {
|
|
6267
|
+
return NftClaimType.Minted;
|
|
6268
|
+
}
|
|
6269
|
+
return NftClaimType.Standard;
|
|
5884
6270
|
}
|
|
5885
6271
|
// ============================================================================
|
|
5886
6272
|
// 奖励值属性提取
|
|
@@ -5906,17 +6292,6 @@ function getGasCoveredBy(value) {
|
|
|
5906
6292
|
}
|
|
5907
6293
|
return undefined;
|
|
5908
6294
|
}
|
|
5909
|
-
/**
|
|
5910
|
-
* 判断是否使用 Gas Station(项目方/TaskOn 代付 Gas)
|
|
5911
|
-
*
|
|
5912
|
-
* @param value - NFT 奖励值
|
|
5913
|
-
* @returns 是否使用 Gas Station
|
|
5914
|
-
*/
|
|
5915
|
-
function shouldUseGasStation(value) {
|
|
5916
|
-
const gasCoveredBy = getGasCoveredBy(value);
|
|
5917
|
-
return (gasCoveredBy === GasCoveredByType$1.Creator ||
|
|
5918
|
-
gasCoveredBy === GasCoveredByType$1.Taskon);
|
|
5919
|
-
}
|
|
5920
6295
|
/**
|
|
5921
6296
|
* 获取 NFT 的 collection_id
|
|
5922
6297
|
*
|
|
@@ -6230,4 +6605,4 @@ function parsePageBuilderConfig(configJson) {
|
|
|
6230
6605
|
}
|
|
6231
6606
|
}
|
|
6232
6607
|
|
|
6233
|
-
export { ApiError, AutomaticallyWinnerDrawType$1 as AutomaticallyWinnerDrawType, CampaignAuditResult, CampaignStatus, CampaignType$1 as CampaignType, Chain$1 as Chain, ChainType, ChangeType, ClaimCapNft, ClaimMintedNft, CommonTemplateAssetType, CommunityStatus, DEFAULT_DISPLAY_OPTIONS, DEFAULT_ERROR_MESSAGES, DEFAULT_PAGE_BUILDER_CONFIG, EligibilityTemplateId, EligibilityType, EligiblityExpression, ErrorCode, FROZEN_ASSETS_PAGE_SIZE, GasCoveredByType$1 as GasCoveredByType,
|
|
6608
|
+
export { ApiError, AutomaticallyWinnerDrawType$1 as AutomaticallyWinnerDrawType, CampaignAuditResult, CampaignStatus, CampaignType$1 as CampaignType, Chain$1 as Chain, ChainType, ChangeType, ClaimCapNft, ClaimMintedNft, CommonTemplateAssetType, CommunityStatus, DEFAULT_DISPLAY_OPTIONS, DEFAULT_ERROR_MESSAGES, DEFAULT_PAGE_BUILDER_CONFIG, EligibilityTemplateId, EligibilityType, EligiblityExpression, ErrorCode, FROZEN_ASSETS_PAGE_SIZE, GasCoveredByType$1 as GasCoveredByType, GetTaskClass, LEADERBOARD_ROWS_OPTIONS, LEADERBOARD_TABLE_COLUMNS_BY_TYPE, LEADERBOARD_TYPE_LABELS, LeaderboardContentType, LeaderboardTableColumn, LeaderboardTableColumnRequirement, LockedType, MediaType$1 as MediaType, MeetConditionStatus, MilestoneMetricType$1 as MilestoneMetricType, NftClaimError, NftClaimErrorType, NftClaimType, NftTransaction, Operator, OuterAPIAccountType, OuterAPISnsType, PAGE_BUILDER_CONFIG_VERSION, PowTaskType, ProjectCategory, AutomaticallyWinnerDrawType as QuestAutomaticallyWinnerDrawType, MilestoneMetricType as QuestMilestoneMetricType, NftType$1 as QuestNftType, RecurrenceType as QuestRecurrenceType, RewardType as QuestRewardType, RewardsDistributeType as QuestRewardsDistributeType, TokenType as QuestTokenType, WinnerDrawType as QuestWinnerDrawType, WinnerRangeType as QuestWinnerRangeType, RecurrenceType$1 as RecurrenceType, RewardDistributedByType$1 as RewardDistributedByType, RewardType, RewardWhiteListType, RewardsDistributeType$1 as RewardsDistributeType, RouletteRewardType, SECTION_LAYOUT_RATIOS, SectionLayoutType, SnsType, TaskCardType, TaskReviewResult, TaskStatus, TaskTemplateId, TokenType$1 as TokenType, USER_CENTER_PAGE_SIZE, USER_CENTER_REWARD_CARD_LABELS, USER_CENTER_REWARD_CARD_TYPES, USER_CENTER_TAB_LABELS, UserCenterAccountType, UserCenterChainType, UserCenterRewardCardType, UserCenterTabType, UserEligibleStatus, UserIdentityType, VerifyCodeType, WidgetTypeEnum, WinnerDrawType$1 as WinnerDrawType, WinnerRangeType$1 as WinnerRangeType, WithdrawNft, apiIcon, bMintedNftAbi, buildSwapTokenText, calculatePrizePool, capNftAbi, checkinIcon, cleanupExpiredPendingHashes, clearAllUserPendingHashes, clearPendingHash, contractInteractiveIcon, createChainApi, createCommonApi, createCommunityTaskApi, createLeaderboardApi, createNftClaimApi, createPageBuilderApi, createQuestApi, createTaskOnClient, createUserApi, createUserCenterApi, createWidgetApi, discordIcon, filterEnabledAccounts, filterEnabledCards, filterEnabledTabs, filterEnabledWallets, formatAddress, formatLocalDate, formatLongNumber, formatRankRange, formatRewardText, formatSwapTokensForDisplay, formatTokenAmount, formatTxHash, formatUtcDuration, formatUtcTime, formatWalletAddress, generateLeaderboardId, generatePendingKey, getChainIcon, getChainName, getCollectionId, getDefaultColumns, getDefaultTabConfig, getDefaultTitle, getGasCoveredBy, getNftChainName, getNftClaimType, getPendingHash, getReceiverAddress, getSocialIcon, getSwapDexTitleExpress, getTxExplorerLink, getTxExplorerUrl, getUserPendingHashes, isAllTimeResponse, isCampaignResponse, isCapNftReward, isManualDrop, isMintedNftReward, isNftClaimable, isNftClaimed, isStandardNftReward, isUnauthorizedError, linkIcon, needsCustomComponent, nftWithdrawAbi, normalizeCampaignInfo, normalizeQuestTask, normalizeQuestTaskInput, normalizeQuestTasks, normalizeTask, normalizeTaskCards, openInNewTab, parsePageBuilderConfig, parseTitleExpress, parseWidgetConfig, powIcon, quizIcon, retweetIcon, setPendingHash, signMessage, telegramIcon, toWei, tokenIcon, truncateAddress, twitterIcon };
|