@subwallet/extension-base 1.3.74-0 → 1.3.75-2

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.
@@ -397,6 +397,9 @@ export declare type RequestSaveBrowserConfig = {
397
397
  export declare type RequestSaveOSConfig = {
398
398
  osConfig: OSConfig;
399
399
  };
400
+ export interface RequestSaveSubscanApiKey {
401
+ apiKey: string;
402
+ }
400
403
  export interface RandomTestRequest {
401
404
  start: number;
402
405
  end: number;
@@ -1976,6 +1979,8 @@ export interface KoniRequestSignatures {
1976
1979
  'pri(settings.saveAppConfig)': [RequestSaveAppConfig, boolean];
1977
1980
  'pri(settings.saveBrowserConfig)': [RequestSaveBrowserConfig, boolean];
1978
1981
  'pri(settings.saveOSConfig)': [RequestSaveOSConfig, boolean];
1982
+ 'pri(settings.saveSubscanApiKey)': [RequestSaveSubscanApiKey, boolean];
1983
+ 'pri(settings.getSubscanApiKey)': [null, string | null];
1979
1984
  'pri(yield.subscribePoolInfo)': [null, YieldPoolInfo[], YieldPoolInfo[]];
1980
1985
  'pri(yield.subscribeYieldPosition)': [null, YieldPositionInfo[], YieldPositionInfo[]];
1981
1986
  'pri(yield.subscribeYieldReward)': [null, EarningRewardJson, EarningRewardJson];
@@ -3,7 +3,7 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.isProductionMode = exports.SW_EXTERNAL_SERVICES_API = exports.BACKEND_API_URL = exports.APP_VERSION = void 0;
6
+ exports.isProductionMode = exports.SW_EXTERNAL_SERVICES_API = exports.SUBSCAN_GATEWAY_URL = exports.BACKEND_API_URL = exports.APP_VERSION = void 0;
7
7
  // Copyright 2019-2022 @subwallet/extension-base authors & contributors
8
8
  // SPDX-License-Identifier: Apache-2.0
9
9
 
@@ -16,4 +16,6 @@ exports.isProductionMode = isProductionMode;
16
16
  const BACKEND_API_URL = process.env.SUBWALLET_API || (isProductionMode ? 'https://sw-services.subwallet.app/api' : 'https://be-dev.subwallet.app/api');
17
17
  exports.BACKEND_API_URL = BACKEND_API_URL;
18
18
  const SW_EXTERNAL_SERVICES_API = process.env.SW_EXTERNAL_SERVICES_API || (isProductionMode ? 'https://external-services.subwallet.app' : 'https://external-services-dev.subwallet.app');
19
- exports.SW_EXTERNAL_SERVICES_API = SW_EXTERNAL_SERVICES_API;
19
+ exports.SW_EXTERNAL_SERVICES_API = SW_EXTERNAL_SERVICES_API;
20
+ const SUBSCAN_GATEWAY_URL = process.env.SUBSCAN_GATEWAY_URL || 'https://gateway-dev.konistudio.xyz';
21
+ exports.SUBSCAN_GATEWAY_URL = SUBSCAN_GATEWAY_URL;
@@ -3884,14 +3884,23 @@ class KoniExtension {
3884
3884
  this.#koniState.saveEnvConfig('osConfig', request.osConfig);
3885
3885
  return true;
3886
3886
  }
3887
+ async saveSubscanApiKey(_ref60) {
3888
+ let {
3889
+ apiKey
3890
+ } = _ref60;
3891
+ return await this.#koniState.saveSubscanApiKey(apiKey);
3892
+ }
3893
+ async getSubscanApiKey() {
3894
+ return await this.#koniState.getSubscanApiKey();
3895
+ }
3887
3896
 
3888
3897
  /// Wallet connect
3889
3898
 
3890
3899
  // Connect
3891
- async connectWalletConnect(_ref60) {
3900
+ async connectWalletConnect(_ref61) {
3892
3901
  let {
3893
3902
  uri
3894
- } = _ref60;
3903
+ } = _ref61;
3895
3904
  await this.#koniState.walletConnectService.connect(uri);
3896
3905
  return true;
3897
3906
  }
@@ -3904,11 +3913,11 @@ class KoniExtension {
3904
3913
  });
3905
3914
  return this.#koniState.requestService.allConnectWCRequests;
3906
3915
  }
3907
- async approveWalletConnectSession(_ref61) {
3916
+ async approveWalletConnectSession(_ref62) {
3908
3917
  let {
3909
3918
  accounts: selectedAccounts,
3910
3919
  id
3911
- } = _ref61;
3920
+ } = _ref62;
3912
3921
  const request = this.#koniState.requestService.getConnectWCRequest(id);
3913
3922
  if ((0, _helpers2.isProposalExpired)(request.request.params)) {
3914
3923
  throw new Error('The proposal has been expired');
@@ -3920,8 +3929,8 @@ class KoniExtension {
3920
3929
  const availableNamespaces = {};
3921
3930
  const namespaces = {};
3922
3931
  const chainInfoMap = this.#koniState.getChainInfoMap();
3923
- Object.entries(requiredNamespaces).forEach(_ref62 => {
3924
- let [key, namespace] = _ref62;
3932
+ Object.entries(requiredNamespaces).forEach(_ref63 => {
3933
+ let [key, namespace] = _ref63;
3925
3934
  if ((0, _helpers2.isSupportWalletConnectNamespace)(key)) {
3926
3935
  if (namespace.chains) {
3927
3936
  const unSupportChains = namespace.chains.filter(chain => !(0, _helpers2.isSupportWalletConnectChain)(chain, chainInfoMap));
@@ -3934,8 +3943,8 @@ class KoniExtension {
3934
3943
  throw new Error((0, _utils1.getSdkError)('UNSUPPORTED_NAMESPACE_KEY').message + ' ' + key);
3935
3944
  }
3936
3945
  });
3937
- Object.entries(optionalNamespaces).forEach(_ref63 => {
3938
- let [key, namespace] = _ref63;
3946
+ Object.entries(optionalNamespaces).forEach(_ref64 => {
3947
+ let [key, namespace] = _ref64;
3939
3948
  if ((0, _helpers2.isSupportWalletConnectNamespace)(key)) {
3940
3949
  if (namespace.chains) {
3941
3950
  const supportChains = namespace.chains.filter(chain => (0, _helpers2.isSupportWalletConnectChain)(chain, chainInfoMap)) || [];
@@ -3959,8 +3968,8 @@ class KoniExtension {
3959
3968
  }
3960
3969
  }
3961
3970
  });
3962
- Object.entries(availableNamespaces).forEach(_ref64 => {
3963
- let [key, namespace] = _ref64;
3971
+ Object.entries(availableNamespaces).forEach(_ref65 => {
3972
+ let [key, namespace] = _ref65;
3964
3973
  if (namespace.chains) {
3965
3974
  const accounts = selectedAccounts.filter(address => {
3966
3975
  const [_namespace] = address.split(':');
@@ -3984,10 +3993,10 @@ class KoniExtension {
3984
3993
  request.resolve();
3985
3994
  return true;
3986
3995
  }
3987
- async rejectWalletConnectSession(_ref65) {
3996
+ async rejectWalletConnectSession(_ref66) {
3988
3997
  let {
3989
3998
  id
3990
- } = _ref65;
3999
+ } = _ref66;
3991
4000
  const request = this.#koniState.requestService.getConnectWCRequest(id);
3992
4001
  const wcId = request.request.id;
3993
4002
  if ((0, _helpers2.isProposalExpired)(request.request.params)) {
@@ -4009,10 +4018,10 @@ class KoniExtension {
4009
4018
  });
4010
4019
  return this.#koniState.walletConnectService.sessions;
4011
4020
  }
4012
- async disconnectWalletConnectSession(_ref66) {
4021
+ async disconnectWalletConnectSession(_ref67) {
4013
4022
  let {
4014
4023
  topic
4015
- } = _ref66;
4024
+ } = _ref67;
4016
4025
  await this.#koniState.walletConnectService.disconnect(topic);
4017
4026
  return true;
4018
4027
  }
@@ -4025,18 +4034,18 @@ class KoniExtension {
4025
4034
  });
4026
4035
  return this.#koniState.requestService.allNotSupportWCRequests;
4027
4036
  }
4028
- approveWalletConnectNotSupport(_ref67) {
4037
+ approveWalletConnectNotSupport(_ref68) {
4029
4038
  let {
4030
4039
  id
4031
- } = _ref67;
4040
+ } = _ref68;
4032
4041
  const request = this.#koniState.requestService.getNotSupportWCRequest(id);
4033
4042
  request.resolve();
4034
4043
  return true;
4035
4044
  }
4036
- rejectWalletConnectNotSupport(_ref68) {
4045
+ rejectWalletConnectNotSupport(_ref69) {
4037
4046
  let {
4038
4047
  id
4039
- } = _ref68;
4048
+ } = _ref69;
4040
4049
  const request = this.#koniState.requestService.getNotSupportWCRequest(id);
4041
4050
  request.reject(new Error('USER_REJECTED'));
4042
4051
  return true;
@@ -4044,11 +4053,11 @@ class KoniExtension {
4044
4053
 
4045
4054
  /// Manta
4046
4055
 
4047
- async enableMantaPay(_ref69) {
4056
+ async enableMantaPay(_ref70) {
4048
4057
  let {
4049
4058
  address,
4050
4059
  password
4051
- } = _ref69;
4060
+ } = _ref70;
4052
4061
  // always takes the current account
4053
4062
  function timeout() {
4054
4063
  return new Promise(resolve => setTimeout(resolve, 1500));
@@ -4138,11 +4147,11 @@ class KoniExtension {
4138
4147
  async disableMantaPay(address) {
4139
4148
  return this.#koniState.disableMantaPay(address);
4140
4149
  }
4141
- async isTonBounceableAddress(_ref70) {
4150
+ async isTonBounceableAddress(_ref71) {
4142
4151
  let {
4143
4152
  address,
4144
4153
  chain
4145
- } = _ref70;
4154
+ } = _ref71;
4146
4155
  try {
4147
4156
  const tonApi = this.#koniState.getTonApi(chain);
4148
4157
  const state = await tonApi.getAccountState(address);
@@ -4188,10 +4197,10 @@ class KoniExtension {
4188
4197
 
4189
4198
  /* Metadata */
4190
4199
 
4191
- async findRawMetadata(_ref71) {
4200
+ async findRawMetadata(_ref72) {
4192
4201
  let {
4193
4202
  genesisHash
4194
- } = _ref71;
4203
+ } = _ref72;
4195
4204
  const {
4196
4205
  metadata,
4197
4206
  specVersion,
@@ -4205,20 +4214,20 @@ class KoniExtension {
4205
4214
  userExtensions
4206
4215
  };
4207
4216
  }
4208
- async calculateMetadataHash(_ref72) {
4217
+ async calculateMetadataHash(_ref73) {
4209
4218
  let {
4210
4219
  chain
4211
- } = _ref72;
4220
+ } = _ref73;
4212
4221
  const hash = await this.#koniState.calculateMetadataHash(chain);
4213
4222
  return {
4214
4223
  metadataHash: hash || ''
4215
4224
  };
4216
4225
  }
4217
- async shortenMetadata(_ref73) {
4226
+ async shortenMetadata(_ref74) {
4218
4227
  let {
4219
4228
  chain,
4220
4229
  txBlob
4221
- } = _ref73;
4230
+ } = _ref74;
4222
4231
  const shorten = await this.#koniState.shortenMetadata(chain, txBlob);
4223
4232
  return {
4224
4233
  txMetadata: shorten || ''
@@ -4595,18 +4604,18 @@ class KoniExtension {
4595
4604
 
4596
4605
  /* Campaign */
4597
4606
 
4598
- unlockDotCheckCanMint(_ref74) {
4607
+ unlockDotCheckCanMint(_ref75) {
4599
4608
  let {
4600
4609
  address,
4601
4610
  network,
4602
4611
  slug
4603
- } = _ref74;
4612
+ } = _ref75;
4604
4613
  return this.#koniState.mintCampaignService.unlockDotCampaign.canMint(address, slug, network);
4605
4614
  }
4606
- unlockDotSubscribeMintedData(id, port, _ref75) {
4615
+ unlockDotSubscribeMintedData(id, port, _ref76) {
4607
4616
  let {
4608
4617
  transactionId
4609
- } = _ref75;
4618
+ } = _ref76;
4610
4619
  const cb = (0, _subscriptions.createSubscription)(id, port);
4611
4620
  const subscription = this.#koniState.mintCampaignService.unlockDotCampaign.subscribeMintedNft(transactionId, cb);
4612
4621
  this.createUnsubscriptionHandle(id, subscription.unsubscribe);
@@ -4638,10 +4647,10 @@ class KoniExtension {
4638
4647
  });
4639
4648
  return filterBanner(await this.#koniState.campaignService.getProcessingCampaign());
4640
4649
  }
4641
- async completeCampaignBanner(_ref76) {
4650
+ async completeCampaignBanner(_ref77) {
4642
4651
  let {
4643
4652
  slug
4644
- } = _ref76;
4653
+ } = _ref77;
4645
4654
  const campaign = await this.#koniState.dbService.getCampaign(slug);
4646
4655
  if (campaign) {
4647
4656
  await this.#koniState.dbService.upsertCampaign({
@@ -5176,8 +5185,8 @@ class KoniExtension {
5176
5185
  resolve();
5177
5186
  }
5178
5187
  };
5179
- this.#koniState.balanceService.subscribeTransferableBalance(address, waitXcmData.chain, waitXcmData.token, waitXcmData.nextTxType, onRs).then(_ref77 => {
5180
- let [_unsub, rs] = _ref77;
5188
+ this.#koniState.balanceService.subscribeTransferableBalance(address, waitXcmData.chain, waitXcmData.token, waitXcmData.nextTxType, onRs).then(_ref78 => {
5189
+ let [_unsub, rs] = _ref78;
5181
5190
  unsub = _unsub;
5182
5191
  onRs(rs);
5183
5192
  }).catch(console.error);
@@ -5838,6 +5847,10 @@ class KoniExtension {
5838
5847
  return this.saveBrowserConfig(request);
5839
5848
  case 'pri(settings.saveOSConfig)':
5840
5849
  return this.saveOSConfig(request);
5850
+ case 'pri(settings.saveSubscanApiKey)':
5851
+ return await this.saveSubscanApiKey(request);
5852
+ case 'pri(settings.getSubscanApiKey)':
5853
+ return await this.getSubscanApiKey();
5841
5854
 
5842
5855
  /// Keyring state
5843
5856
  case 'pri(keyring.subscribe)':
@@ -69,6 +69,8 @@ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r
69
69
  // eslint-disable-next-line @typescript-eslint/no-var-requires,@typescript-eslint/no-unsafe-assignment
70
70
  const passworder = require('browser-passworder');
71
71
  const ERROR_CONFIRMATION_TYPE = ['errorConnectNetwork'];
72
+ const SUBSCAN_API_KEY_STORAGE = 'subscan_api_key';
73
+ const SUBSCAN_SECRET_STORAGE = 'subscan_secret';
72
74
 
73
75
  // List of providers passed into constructor. This is the list of providers
74
76
  // exposed by the extension.
@@ -228,6 +230,8 @@ class KoniState {
228
230
  }
229
231
  afterChainServiceInit() {
230
232
  this.subscanService.setSubscanChainMap(this.chainService.getSubscanChainMap());
233
+ // Sync Subscan API key
234
+ this.syncSubscanApiKey().catch(console.error);
231
235
  }
232
236
  async init() {
233
237
  await this.eventService.waitCryptoReady;
@@ -1612,6 +1616,84 @@ class KoniState {
1612
1616
  return null;
1613
1617
  }
1614
1618
  }
1619
+ async getExtensionSecret() {
1620
+ const result = await _storage.SWStorage.instance.getItem(SUBSCAN_SECRET_STORAGE);
1621
+ let secret = result;
1622
+ if (!secret) {
1623
+ secret = crypto.randomUUID();
1624
+ await _storage.SWStorage.instance.setItem(SUBSCAN_SECRET_STORAGE, secret);
1625
+ }
1626
+ return secret;
1627
+ }
1628
+
1629
+ // Generate password used to encrypt/decrypt Subscan API key
1630
+ // Password = extensionId + extension secret
1631
+ // -> Bind encrypted data to THIS extension instance
1632
+ // -> Prevent other extensions/apps from decrypting the stored API key
1633
+ async getSubscanApiCipherPassword() {
1634
+ var _chrome, _chrome$runtime;
1635
+ const secret = await this.getExtensionSecret();
1636
+ const extensionId = ((_chrome = chrome) === null || _chrome === void 0 ? void 0 : (_chrome$runtime = _chrome.runtime) === null || _chrome$runtime === void 0 ? void 0 : _chrome$runtime.id) || 'subwallet';
1637
+ return `${extensionId}:${secret}`;
1638
+ }
1639
+ async saveSubscanApiKey(apiKey) {
1640
+ try {
1641
+ // Get cipher password used for encryption
1642
+ const cipherPassword = await this.getSubscanApiCipherPassword();
1643
+
1644
+ // Encrypt API key before saving to storage
1645
+ // -> Avoid storing API key as plain text
1646
+ // -> Prevent leakage if storage is inspected
1647
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment
1648
+ const encryptedData = await passworder.encrypt(cipherPassword, {
1649
+ apiKey
1650
+ });
1651
+
1652
+ // Persist encrypted API key to extension storage
1653
+ await _storage.SWStorage.instance.setItem(SUBSCAN_API_KEY_STORAGE, JSON.stringify(encryptedData));
1654
+
1655
+ // Sync API key to Subscan service instance
1656
+ // -> Ensure API calls immediately use the latest key
1657
+ await this.syncSubscanApiKey();
1658
+ return true;
1659
+ } catch (e) {
1660
+ console.error(e);
1661
+ return false;
1662
+ }
1663
+ }
1664
+ async getSubscanApiKey() {
1665
+ try {
1666
+ // Read encrypted API key from storage
1667
+ const encryptedData = await _storage.SWStorage.instance.getItem(SUBSCAN_API_KEY_STORAGE);
1668
+ if (!encryptedData) {
1669
+ return null;
1670
+ }
1671
+
1672
+ // Recreate cipher password for decryption
1673
+ const cipherPassword = await this.getSubscanApiCipherPassword();
1674
+
1675
+ // Decrypt stored API key
1676
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment
1677
+ const decryptedData = await passworder.decrypt(cipherPassword, JSON.parse(encryptedData));
1678
+
1679
+ // Return API key if exists
1680
+ return decryptedData.apiKey || null;
1681
+ } catch (e) {
1682
+ console.error(e);
1683
+ return null;
1684
+ }
1685
+ }
1686
+
1687
+ // Sync decrypted API key to Subscan service
1688
+ // -> Allows service layer to use API key for authenticated requests
1689
+ async syncSubscanApiKey() {
1690
+ try {
1691
+ const apiKey = await this.getSubscanApiKey();
1692
+ this.subscanService.setApiKey(apiKey);
1693
+ } catch (e) {
1694
+ console.error('Failed to sync Subscan API key:', e);
1695
+ }
1696
+ }
1615
1697
  onCheckToRemindUser() {
1616
1698
  this.onHandleRemindExportAccount().catch(console.error);
1617
1699
  }
@@ -13,6 +13,6 @@ const packageInfo = {
13
13
  name: '@subwallet/extension-base',
14
14
  path: typeof __dirname === 'string' ? __dirname : 'auto',
15
15
  type: 'cjs',
16
- version: '1.3.74-0'
16
+ version: '1.3.75-2'
17
17
  };
18
18
  exports.packageInfo = packageInfo;
@@ -5,7 +5,8 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.SubscanService = void 0;
7
7
  var _SWError = require("@subwallet/extension-base/background/errors/SWError");
8
- var _constants = require("@subwallet/extension-base/koni/api/nft/ordinal_nft/constants");
8
+ var _constants = require("@subwallet/extension-base/constants");
9
+ var _constants2 = require("@subwallet/extension-base/koni/api/nft/ordinal_nft/constants");
9
10
  var _subscanChainMap = require("@subwallet/extension-base/services/subscan-service/subscan-chain-map");
10
11
  var _base = require("@subwallet/extension-base/strategy/api-request-strategy/context/base");
11
12
  var _apiRequestStrategyV = require("@subwallet/extension-base/strategy/api-request-strategy-v2");
@@ -15,11 +16,15 @@ var _utils = require("@subwallet/extension-base/utils");
15
16
 
16
17
  const QUERY_ROW = 100;
17
18
  class SubscanService extends _apiRequestStrategyV.BaseApiRequestStrategyV2 {
19
+ apiKey = null;
18
20
  constructor(subscanChainMap, options) {
19
21
  const context = new _base.BaseApiRequestContext(options);
20
22
  super(context);
21
23
  this.subscanChainMap = subscanChainMap;
22
24
  }
25
+ setApiKey(key) {
26
+ this.apiKey = key;
27
+ }
23
28
  getApiUrl(chain, path) {
24
29
  const subscanChain = this.subscanChainMap[chain];
25
30
  if (!subscanChain) {
@@ -28,11 +33,26 @@ class SubscanService extends _apiRequestStrategyV.BaseApiRequestStrategyV2 {
28
33
  return `https://${subscanChain}.api.subscan.io/${path}`;
29
34
  }
30
35
  postRequest(url, body) {
36
+ const parsed = new URL(url);
37
+ const headers = {
38
+ 'Content-Type': 'application/json'
39
+ };
40
+ if (this.apiKey) {
41
+ headers['X-API-Key'] = this.apiKey;
42
+ }
43
+ if (_utils.targetIsWeb) {
44
+ const suffix = '.api.subscan.io';
45
+ const subscanChain = parsed.hostname.endsWith(suffix) ? parsed.hostname.slice(0, -suffix.length) : parsed.hostname;
46
+ headers['x-network'] = subscanChain;
47
+ return fetch(`${_constants.SUBSCAN_GATEWAY_URL}${parsed.pathname}${parsed.search}`, {
48
+ method: 'POST',
49
+ headers,
50
+ body: JSON.stringify(body)
51
+ });
52
+ }
31
53
  return fetch(url, {
32
54
  method: 'POST',
33
- headers: {
34
- 'Content-Type': 'application/json'
35
- },
55
+ headers,
36
56
  body: JSON.stringify(body)
37
57
  });
38
58
  }
@@ -244,7 +264,7 @@ class SubscanService extends _apiRequestStrategyV.BaseApiRequestStrategyV2 {
244
264
  getAccountRemarkEvents(groupId, chain, address) {
245
265
  return this.addRequest(async () => {
246
266
  const rs = await this.postRequest(this.getApiUrl(chain, 'api/v2/scan/events'), {
247
- ..._constants.BASE_FETCH_ORDINAL_EVENT_DATA,
267
+ ..._constants2.BASE_FETCH_ORDINAL_EVENT_DATA,
248
268
  address
249
269
  });
250
270
  if (rs.status !== 200) {
@@ -272,7 +292,15 @@ class SubscanService extends _apiRequestStrategyV.BaseApiRequestStrategyV2 {
272
292
 
273
293
  static getInstance() {
274
294
  if (!SubscanService._instance) {
275
- SubscanService._instance = new SubscanService(_subscanChainMap.SUBSCAN_API_CHAIN_MAP);
295
+ // Subscan API allows only ~2 requests per second.
296
+ // However, each request from the webapp also triggers an OPTIONS request (CORS preflight),
297
+ // which Subscan counts towards the quota as well → effectively 1 call = 2 requests.
298
+ // To avoid hitting the rate limit, we configure the queue
299
+ // to allow only 1 request per second.
300
+ SubscanService._instance = new SubscanService(_subscanChainMap.SUBSCAN_API_CHAIN_MAP, {
301
+ limitRate: 1,
302
+ intervalCheck: 1000
303
+ });
276
304
  }
277
305
  return SubscanService._instance;
278
306
  }
@@ -2,3 +2,4 @@ export declare const APP_VERSION: string;
2
2
  export declare const isProductionMode: boolean;
3
3
  export declare const BACKEND_API_URL: string;
4
4
  export declare const SW_EXTERNAL_SERVICES_API: string;
5
+ export declare const SUBSCAN_GATEWAY_URL: string;
@@ -6,4 +6,5 @@ const branchName = process.env.BRANCH_NAME || 'subwallet-dev';
6
6
  export const APP_VERSION = process.env.PKG_VERSION || '';
7
7
  export const isProductionMode = PRODUCTION_BRANCHES.indexOf(branchName) > -1;
8
8
  export const BACKEND_API_URL = process.env.SUBWALLET_API || (isProductionMode ? 'https://sw-services.subwallet.app/api' : 'https://be-dev.subwallet.app/api');
9
- export const SW_EXTERNAL_SERVICES_API = process.env.SW_EXTERNAL_SERVICES_API || (isProductionMode ? 'https://external-services.subwallet.app' : 'https://external-services-dev.subwallet.app');
9
+ export const SW_EXTERNAL_SERVICES_API = process.env.SW_EXTERNAL_SERVICES_API || (isProductionMode ? 'https://external-services.subwallet.app' : 'https://external-services-dev.subwallet.app');
10
+ export const SUBSCAN_GATEWAY_URL = process.env.SUBSCAN_GATEWAY_URL || 'https://gateway-dev.konistudio.xyz';
@@ -225,6 +225,8 @@ export default class KoniExtension {
225
225
  private saveAppConfig;
226
226
  private saveBrowserConfig;
227
227
  private saveOSConfig;
228
+ private saveSubscanApiKey;
229
+ private getSubscanApiKey;
228
230
  private connectWalletConnect;
229
231
  private connectWCSubscribe;
230
232
  private approveWalletConnectSession;
@@ -3806,6 +3806,14 @@ export default class KoniExtension {
3806
3806
  this.#koniState.saveEnvConfig('osConfig', request.osConfig);
3807
3807
  return true;
3808
3808
  }
3809
+ async saveSubscanApiKey({
3810
+ apiKey
3811
+ }) {
3812
+ return await this.#koniState.saveSubscanApiKey(apiKey);
3813
+ }
3814
+ async getSubscanApiKey() {
3815
+ return await this.#koniState.getSubscanApiKey();
3816
+ }
3809
3817
 
3810
3818
  /// Wallet connect
3811
3819
 
@@ -5741,6 +5749,10 @@ export default class KoniExtension {
5741
5749
  return this.saveBrowserConfig(request);
5742
5750
  case 'pri(settings.saveOSConfig)':
5743
5751
  return this.saveOSConfig(request);
5752
+ case 'pri(settings.saveSubscanApiKey)':
5753
+ return await this.saveSubscanApiKey(request);
5754
+ case 'pri(settings.getSubscanApiKey)':
5755
+ return await this.getSubscanApiKey();
5744
5756
 
5745
5757
  /// Keyring state
5746
5758
  case 'pri(keyring.subscribe)':
@@ -252,6 +252,11 @@ export default class KoniState {
252
252
  private onHandleRemindExportAccount;
253
253
  setStorageFromWS({ key, value }: StorageDataInterface): Promise<boolean>;
254
254
  getStorageFromWS(key: string): Promise<string | null>;
255
+ private getExtensionSecret;
256
+ private getSubscanApiCipherPassword;
257
+ saveSubscanApiKey(apiKey: string): Promise<boolean>;
258
+ getSubscanApiKey(): Promise<string | null>;
259
+ private syncSubscanApiKey;
255
260
  onCheckToRemindUser(): void;
256
261
  onInstall(): void;
257
262
  get activeNetworks(): Record<string, _ChainInfo>;
@@ -62,6 +62,8 @@ import { KoniSubscription } from "../subscription.js";
62
62
  // eslint-disable-next-line @typescript-eslint/no-var-requires,@typescript-eslint/no-unsafe-assignment
63
63
  const passworder = require('browser-passworder');
64
64
  const ERROR_CONFIRMATION_TYPE = ['errorConnectNetwork'];
65
+ const SUBSCAN_API_KEY_STORAGE = 'subscan_api_key';
66
+ const SUBSCAN_SECRET_STORAGE = 'subscan_secret';
65
67
 
66
68
  // List of providers passed into constructor. This is the list of providers
67
69
  // exposed by the extension.
@@ -219,6 +221,8 @@ export default class KoniState {
219
221
  }
220
222
  afterChainServiceInit() {
221
223
  this.subscanService.setSubscanChainMap(this.chainService.getSubscanChainMap());
224
+ // Sync Subscan API key
225
+ this.syncSubscanApiKey().catch(console.error);
222
226
  }
223
227
  async init() {
224
228
  await this.eventService.waitCryptoReady;
@@ -1581,6 +1585,84 @@ export default class KoniState {
1581
1585
  return null;
1582
1586
  }
1583
1587
  }
1588
+ async getExtensionSecret() {
1589
+ const result = await SWStorage.instance.getItem(SUBSCAN_SECRET_STORAGE);
1590
+ let secret = result;
1591
+ if (!secret) {
1592
+ secret = crypto.randomUUID();
1593
+ await SWStorage.instance.setItem(SUBSCAN_SECRET_STORAGE, secret);
1594
+ }
1595
+ return secret;
1596
+ }
1597
+
1598
+ // Generate password used to encrypt/decrypt Subscan API key
1599
+ // Password = extensionId + extension secret
1600
+ // -> Bind encrypted data to THIS extension instance
1601
+ // -> Prevent other extensions/apps from decrypting the stored API key
1602
+ async getSubscanApiCipherPassword() {
1603
+ var _chrome, _chrome$runtime;
1604
+ const secret = await this.getExtensionSecret();
1605
+ const extensionId = ((_chrome = chrome) === null || _chrome === void 0 ? void 0 : (_chrome$runtime = _chrome.runtime) === null || _chrome$runtime === void 0 ? void 0 : _chrome$runtime.id) || 'subwallet';
1606
+ return `${extensionId}:${secret}`;
1607
+ }
1608
+ async saveSubscanApiKey(apiKey) {
1609
+ try {
1610
+ // Get cipher password used for encryption
1611
+ const cipherPassword = await this.getSubscanApiCipherPassword();
1612
+
1613
+ // Encrypt API key before saving to storage
1614
+ // -> Avoid storing API key as plain text
1615
+ // -> Prevent leakage if storage is inspected
1616
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment
1617
+ const encryptedData = await passworder.encrypt(cipherPassword, {
1618
+ apiKey
1619
+ });
1620
+
1621
+ // Persist encrypted API key to extension storage
1622
+ await SWStorage.instance.setItem(SUBSCAN_API_KEY_STORAGE, JSON.stringify(encryptedData));
1623
+
1624
+ // Sync API key to Subscan service instance
1625
+ // -> Ensure API calls immediately use the latest key
1626
+ await this.syncSubscanApiKey();
1627
+ return true;
1628
+ } catch (e) {
1629
+ console.error(e);
1630
+ return false;
1631
+ }
1632
+ }
1633
+ async getSubscanApiKey() {
1634
+ try {
1635
+ // Read encrypted API key from storage
1636
+ const encryptedData = await SWStorage.instance.getItem(SUBSCAN_API_KEY_STORAGE);
1637
+ if (!encryptedData) {
1638
+ return null;
1639
+ }
1640
+
1641
+ // Recreate cipher password for decryption
1642
+ const cipherPassword = await this.getSubscanApiCipherPassword();
1643
+
1644
+ // Decrypt stored API key
1645
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment
1646
+ const decryptedData = await passworder.decrypt(cipherPassword, JSON.parse(encryptedData));
1647
+
1648
+ // Return API key if exists
1649
+ return decryptedData.apiKey || null;
1650
+ } catch (e) {
1651
+ console.error(e);
1652
+ return null;
1653
+ }
1654
+ }
1655
+
1656
+ // Sync decrypted API key to Subscan service
1657
+ // -> Allows service layer to use API key for authenticated requests
1658
+ async syncSubscanApiKey() {
1659
+ try {
1660
+ const apiKey = await this.getSubscanApiKey();
1661
+ this.subscanService.setApiKey(apiKey);
1662
+ } catch (e) {
1663
+ console.error('Failed to sync Subscan API key:', e);
1664
+ }
1665
+ }
1584
1666
  onCheckToRemindUser() {
1585
1667
  this.onHandleRemindExportAccount().catch(console.error);
1586
1668
  }
package/package.json CHANGED
@@ -17,7 +17,7 @@
17
17
  "./cjs/detectPackage.js"
18
18
  ],
19
19
  "type": "module",
20
- "version": "1.3.74-0",
20
+ "version": "1.3.75-2",
21
21
  "main": "./cjs/index.js",
22
22
  "module": "./index.js",
23
23
  "types": "./index.d.ts",
@@ -3008,11 +3008,11 @@
3008
3008
  "@sora-substrate/type-definitions": "^1.17.7",
3009
3009
  "@substrate/connect": "^0.8.9",
3010
3010
  "@subwallet-monorepos/subwallet-services-sdk": "0.1.16",
3011
- "@subwallet/chain-list": "0.2.124-beta.1",
3012
- "@subwallet/extension-base": "^1.3.74-0",
3013
- "@subwallet/extension-chains": "^1.3.74-0",
3014
- "@subwallet/extension-dapp": "^1.3.74-0",
3015
- "@subwallet/extension-inject": "^1.3.74-0",
3011
+ "@subwallet/chain-list": "0.2.124",
3012
+ "@subwallet/extension-base": "^1.3.75-2",
3013
+ "@subwallet/extension-chains": "^1.3.75-2",
3014
+ "@subwallet/extension-dapp": "^1.3.75-2",
3015
+ "@subwallet/extension-inject": "^1.3.75-2",
3016
3016
  "@subwallet/keyring": "^0.1.14",
3017
3017
  "@subwallet/ui-keyring": "^0.1.14",
3018
3018
  "@ton/core": "^0.56.3",
package/packageInfo.js CHANGED
@@ -7,5 +7,5 @@ export const packageInfo = {
7
7
  name: '@subwallet/extension-base',
8
8
  path: (import.meta && import.meta.url) ? new URL(import.meta.url).pathname.substring(0, new URL(import.meta.url).pathname.lastIndexOf('/') + 1) : 'auto',
9
9
  type: 'esm',
10
- version: '1.3.74-0'
10
+ version: '1.3.75-2'
11
11
  };
@@ -4,7 +4,9 @@ import { BaseApiRequestStrategyV2 } from '@subwallet/extension-base/strategy/api
4
4
  import { SubscanEventBaseItemData, SubscanExtrinsicParam } from '@subwallet/extension-base/types';
5
5
  export declare class SubscanService extends BaseApiRequestStrategyV2 {
6
6
  private subscanChainMap;
7
+ private apiKey;
7
8
  constructor(subscanChainMap: Record<string, string>, options?: Partial<ApiRequestContextProps>);
9
+ setApiKey(key: string | null): void;
8
10
  private getApiUrl;
9
11
  private postRequest;
10
12
  isRateLimited(e: Error): boolean;
@@ -2,18 +2,23 @@
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
 
4
4
  import { SWError } from '@subwallet/extension-base/background/errors/SWError';
5
+ import { SUBSCAN_GATEWAY_URL } from '@subwallet/extension-base/constants';
5
6
  import { BASE_FETCH_ORDINAL_EVENT_DATA } from '@subwallet/extension-base/koni/api/nft/ordinal_nft/constants';
6
7
  import { SUBSCAN_API_CHAIN_MAP } from '@subwallet/extension-base/services/subscan-service/subscan-chain-map';
7
8
  import { BaseApiRequestContext } from '@subwallet/extension-base/strategy/api-request-strategy/context/base';
8
9
  import { BaseApiRequestStrategyV2 } from '@subwallet/extension-base/strategy/api-request-strategy-v2';
9
- import { wait } from '@subwallet/extension-base/utils';
10
+ import { targetIsWeb, wait } from '@subwallet/extension-base/utils';
10
11
  const QUERY_ROW = 100;
11
12
  export class SubscanService extends BaseApiRequestStrategyV2 {
13
+ apiKey = null;
12
14
  constructor(subscanChainMap, options) {
13
15
  const context = new BaseApiRequestContext(options);
14
16
  super(context);
15
17
  this.subscanChainMap = subscanChainMap;
16
18
  }
19
+ setApiKey(key) {
20
+ this.apiKey = key;
21
+ }
17
22
  getApiUrl(chain, path) {
18
23
  const subscanChain = this.subscanChainMap[chain];
19
24
  if (!subscanChain) {
@@ -22,11 +27,26 @@ export class SubscanService extends BaseApiRequestStrategyV2 {
22
27
  return `https://${subscanChain}.api.subscan.io/${path}`;
23
28
  }
24
29
  postRequest(url, body) {
30
+ const parsed = new URL(url);
31
+ const headers = {
32
+ 'Content-Type': 'application/json'
33
+ };
34
+ if (this.apiKey) {
35
+ headers['X-API-Key'] = this.apiKey;
36
+ }
37
+ if (targetIsWeb) {
38
+ const suffix = '.api.subscan.io';
39
+ const subscanChain = parsed.hostname.endsWith(suffix) ? parsed.hostname.slice(0, -suffix.length) : parsed.hostname;
40
+ headers['x-network'] = subscanChain;
41
+ return fetch(`${SUBSCAN_GATEWAY_URL}${parsed.pathname}${parsed.search}`, {
42
+ method: 'POST',
43
+ headers,
44
+ body: JSON.stringify(body)
45
+ });
46
+ }
25
47
  return fetch(url, {
26
48
  method: 'POST',
27
- headers: {
28
- 'Content-Type': 'application/json'
29
- },
49
+ headers,
30
50
  body: JSON.stringify(body)
31
51
  });
32
52
  }
@@ -256,7 +276,15 @@ export class SubscanService extends BaseApiRequestStrategyV2 {
256
276
 
257
277
  static getInstance() {
258
278
  if (!SubscanService._instance) {
259
- SubscanService._instance = new SubscanService(SUBSCAN_API_CHAIN_MAP);
279
+ // Subscan API allows only ~2 requests per second.
280
+ // However, each request from the webapp also triggers an OPTIONS request (CORS preflight),
281
+ // which Subscan counts towards the quota as well → effectively 1 call = 2 requests.
282
+ // To avoid hitting the rate limit, we configure the queue
283
+ // to allow only 1 request per second.
284
+ SubscanService._instance = new SubscanService(SUBSCAN_API_CHAIN_MAP, {
285
+ limitRate: 1,
286
+ intervalCheck: 1000
287
+ });
260
288
  }
261
289
  return SubscanService._instance;
262
290
  }