@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 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
- const timeoutId = setTimeout(() => controller.abort(), timeout);
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
- throw err;
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
- const result = (await response.json());
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
- ? `Reply tweet & tag ${friendsCount} friends`
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
- return normalizeTaskParams(task, templateValue);
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
- ? `Reply tweet with hashtag & tag ${friendsCount} friends`
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
- return normalizeTaskParams(task, templateValue);
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
- const normalized = normalizeTaskParams(task, templateValue);
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
- // OuterAPI 总是作为富文本处理,与 Vue 版本保持一致
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
- // OuterPointAPI 总是作为富文本处理,与 Vue 版本保持一致
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
- titleExpress = `Reply {this tweet}[link=${tweetLink}] & tag ${friendsCount} friends on Twitter`;
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
- titleExpress = `Reply {this tweet}[link=${tweetLink}] & tag ${friendsCount} friends & include "${hashtagDisplay}" on Twitter`;
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 ${friendsCount} friends on Twitter`;
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
- * - 描述始终以富文本方式渲染(is_rich_text: true)
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 tokenWithdrawFromGasStation(params) {
4522
+ async getTokenWithdrawByNonce(params) {
4468
4523
  const res = await client.request({
4469
- url: "/v1/tokenWithdrawFromGasStation",
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
- // =========== Gas Station ===========
4998
- async claimCampaignRewardFromGasStation(params) {
4999
- await client.request({
5000
- url: "/v1/claimCampaignRewardFromGasStation",
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 = error instanceof Error ? error.message : String(error);
5141
+ const message = NftClaimError.extractErrorMessage(error);
5094
5142
  const cause = error instanceof Error ? error : undefined;
5095
5143
  // 尝试识别常见错误类型
5096
- const type = NftClaimError.inferErrorType(message);
5097
- return new NftClaimError(type, message, cause);
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 (lowerMessage.includes("user rejected") ||
5156
+ if (NftClaimError.hasUserRejectedCode(error) ||
5157
+ lowerMessage.includes("user rejected") ||
5106
5158
  lowerMessage.includes("user denied") ||
5107
- lowerMessage.includes("cancelled")) {
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.sigRes.contract_address, data);
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.sigRes.contract_address,
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
- * - Minted NFT: `minted-${campaignId}-${rewardId}`
5643
- * - CAP NFT: `cap-${campaignId}-${rewardId}`
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 nftType - NFT 类型(minted / cap)
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(nftType, campaignId, rewardId) {
5700
- return `${nftType}-${campaignId}-${rewardId}`;
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 信息,如果不存在或已过期则返回 null
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
- if (Date.now() - pending.timestamp > PENDING_EXPIRE_TIME) {
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
- const userPending = map[userId] || {};
5771
- const result = {};
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
- const map = getPendingHashMap();
5798
- let hasChanges = false;
5799
- for (const userId of Object.keys(map)) {
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 CAP 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 ("gas_covered_by" in value &&
5873
- !("nft_address" in value));
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
- return exports.NftClaimType.Minted;
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;