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