@silentswap/sdk 0.1.56 → 0.1.58

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/assets.d.ts CHANGED
@@ -31,14 +31,18 @@ export declare function loadAssetsData(): AssetsData;
31
31
  export declare function getAllAssets(): Record<Caip19, AssetInfo>;
32
32
  export declare function getAllChains(): Record<Caip2, ChainInfo>;
33
33
  export declare function getAllAssetsArray(): AssetInfo[];
34
+ export declare function getCommonAssets(): AssetInfo[];
34
35
  export declare const COMMON_ASSETS: AssetInfo[];
35
36
  export declare const CHAIN_NAMES: Record<string, string>;
36
37
  /**
37
- * Get asset by CAIP-19 identifier from the full dataset
38
+ * Get asset by CAIP-19 identifier from the full dataset.
39
+ * O(1) after first call; returns the same reference across calls
40
+ * so consumers can rely on referential equality.
38
41
  */
39
42
  export declare function getAssetByCaip19(caip19: Caip19): AssetInfo | undefined;
40
43
  /**
41
- * Get chain info by CAIP-2 identifier
44
+ * Get chain info by CAIP-2 identifier.
45
+ * Uses the underlying object as a cache; record lookup is already O(1).
42
46
  */
43
47
  export declare function getChainByCaip2(caip2: Caip2): ChainInfo | undefined;
44
48
  /**
package/dist/assets.js CHANGED
@@ -1,111 +1,208 @@
1
1
  import filteredData from './data/filtered.json' with { type: 'json' };
2
+ function getFilteredData() {
3
+ return filteredData;
4
+ }
2
5
  // Load assets data from filtered.json
3
6
  export function loadAssetsData() {
4
- return filteredData;
7
+ return getFilteredData();
5
8
  }
6
9
  // Get all assets from the filtered data
7
10
  export function getAllAssets() {
8
- return filteredData.assets;
11
+ return getFilteredData().assets;
9
12
  }
10
13
  // Get all chains from the filtered data
11
14
  export function getAllChains() {
12
- return filteredData.chains;
15
+ return getFilteredData().chains;
13
16
  }
17
+ // Cached array of all assets — avoids re-mapping on every call
18
+ let _allAssetsArray = null;
14
19
  // Get all assets as an array for easy iteration
15
20
  export function getAllAssetsArray() {
16
- const assets = filteredData.assets;
17
- return Object.values(assets).map(asset => ({
21
+ if (_allAssetsArray)
22
+ return _allAssetsArray;
23
+ const assets = getFilteredData().assets;
24
+ _allAssetsArray = Object.values(assets).map(asset => ({
18
25
  ...asset,
19
26
  gradient: asset.gradient.length >= 2
20
27
  ? [asset.gradient[0], asset.gradient[1]]
21
28
  : ['000000', '000000'],
22
29
  caip19: asset.caip19,
23
30
  }));
31
+ return _allAssetsArray;
24
32
  }
25
- // Common tokens list - frequently used assets for quick access
33
+ // Common CAIP-19 identifiers for popular tokens
26
34
  // This matches the popular tokens list in the Svelte UI (PopularTokensArea.svelte)
27
- export const COMMON_ASSETS = getAllAssetsArray().filter(asset => {
28
- // Filter to commonly used tokens (matches Svelte PopularTokensArea.svelte)
29
- const commonCaip19s = [
30
- 'tron:0x2b6653dc/trc20:TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t', // USDT on Tron
31
- 'bip122:000000000019d6689c085ae165831e93/slip44:0', // BTC on Bitcoin
32
- 'eip155:1/slip44:60', // ETH on Ethereum
33
- 'eip155:1/erc20:0xdAC17F958D2ee523a2206206994597C13D831ec7', // USDT on Ethereum
34
- 'eip155:1/erc20:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC on Ethereum
35
- 'eip155:1/erc20:0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', // WETH on Ethereum
36
- 'eip155:1/erc20:0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599', // WBTC on Ethereum
37
- 'eip155:1/erc20:0xae78736Cd615f374D3085123A210448E74Fc6393', // RETH on Ethereum
38
- 'eip155:56/slip44:714', // BNB on BSC
39
- 'eip155:56/erc20:0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c', // WBNB on BSC
40
- 'eip155:8453/slip44:60', // ETH on Base
41
- 'eip155:8453/erc20:0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', // USDC on Base
42
- 'eip155:43114/slip44:9005', // WAVAX on Avalanche
43
- 'eip155:43114/erc20:0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E', // USDC on Avalanche
44
- 'eip155:137/slip44:966', // WMATIC/WPOL on Polygon
45
- 'eip155:137/erc20:0xc2132D05D31c914a87C6611C10748AEb04B58e8F', // USDT on Polygon
46
- 'eip155:137/erc20:0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359', // USDC on Polygon
47
- 'eip155:10/slip44:60', // WETH on Optimism
48
- 'eip155:10/erc20:0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85', // USDC on Optimism
49
- 'eip155:42161/slip44:60', // WETH on Arbitrum
50
- 'eip155:42161/erc20:0xaf88d065e77c8cC2239327C5EDb3A432268e5831', // USDC on Arbitrum
51
- ];
52
- return commonCaip19s.includes(asset.caip19);
35
+ const COMMON_CAIP19S = new Set([
36
+ 'tron:0x2b6653dc/trc20:TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t', // USDT on Tron
37
+ 'bip122:000000000019d6689c085ae165831e93/slip44:0', // BTC on Bitcoin
38
+ 'eip155:1/slip44:60', // ETH on Ethereum
39
+ 'eip155:1/erc20:0xdAC17F958D2ee523a2206206994597C13D831ec7', // USDT on Ethereum
40
+ 'eip155:1/erc20:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC on Ethereum
41
+ 'eip155:1/erc20:0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', // WETH on Ethereum
42
+ 'eip155:1/erc20:0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599', // WBTC on Ethereum
43
+ 'eip155:1/erc20:0xae78736Cd615f374D3085123A210448E74Fc6393', // RETH on Ethereum
44
+ 'eip155:56/slip44:714', // BNB on BSC
45
+ 'eip155:56/erc20:0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c', // WBNB on BSC
46
+ 'eip155:8453/slip44:60', // ETH on Base
47
+ 'eip155:8453/erc20:0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', // USDC on Base
48
+ 'eip155:43114/slip44:9005', // WAVAX on Avalanche
49
+ 'eip155:43114/erc20:0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E', // USDC on Avalanche
50
+ 'eip155:137/slip44:966', // WMATIC/WPOL on Polygon
51
+ 'eip155:137/erc20:0xc2132D05D31c914a87C6611C10748AEb04B58e8F', // USDT on Polygon
52
+ 'eip155:137/erc20:0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359', // USDC on Polygon
53
+ 'eip155:10/slip44:60', // WETH on Optimism
54
+ 'eip155:10/erc20:0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85', // USDC on Optimism
55
+ 'eip155:42161/slip44:60', // WETH on Arbitrum
56
+ 'eip155:42161/erc20:0xaf88d065e77c8cC2239327C5EDb3A432268e5831', // USDC on Arbitrum
57
+ ]);
58
+ // Common tokens list - frequently used assets for quick access (lazy)
59
+ let _commonAssets = null;
60
+ export function getCommonAssets() {
61
+ if (_commonAssets)
62
+ return _commonAssets;
63
+ _commonAssets = getAllAssetsArray().filter(asset => COMMON_CAIP19S.has(asset.caip19));
64
+ return _commonAssets;
65
+ }
66
+ // Keep COMMON_ASSETS as a getter for backward compatibility
67
+ export const COMMON_ASSETS = new Proxy([], {
68
+ get(target, prop, receiver) {
69
+ const real = getCommonAssets();
70
+ return Reflect.get(real, prop, receiver);
71
+ },
72
+ has(_target, prop) {
73
+ return prop in getCommonAssets();
74
+ },
75
+ ownKeys() {
76
+ return Reflect.ownKeys(getCommonAssets());
77
+ },
78
+ getOwnPropertyDescriptor(_target, prop) {
79
+ return Object.getOwnPropertyDescriptor(getCommonAssets(), prop);
80
+ },
81
+ set(_target, _prop, _value) {
82
+ throw new TypeError('COMMON_ASSETS is read-only');
83
+ },
84
+ deleteProperty(_target, _prop) {
85
+ throw new TypeError('COMMON_ASSETS is read-only');
86
+ },
53
87
  });
54
- // Build chain names map from filtered data
55
- export const CHAIN_NAMES = Object.fromEntries(Object.entries(filteredData.chains).map(([caip2, chain]) => {
56
- const match = caip2.match(/^eip155:(\d+)$/);
57
- if (match) {
58
- return [match[1], chain.name];
59
- }
60
- return [];
61
- }).filter((entry) => entry.length > 0));
88
+ // Build chain names map from filtered data (lazy)
89
+ let _chainNames = null;
90
+ function getChainNames() {
91
+ if (_chainNames)
92
+ return _chainNames;
93
+ _chainNames = Object.fromEntries(Object.entries(getFilteredData().chains).map(([caip2, chain]) => {
94
+ const match = caip2.match(/^eip155:(\d+)$/);
95
+ if (match) {
96
+ return [match[1], chain.name];
97
+ }
98
+ return [];
99
+ }).filter((entry) => entry.length > 0));
100
+ return _chainNames;
101
+ }
102
+ export const CHAIN_NAMES = new Proxy({}, {
103
+ get(_target, prop, receiver) {
104
+ return Reflect.get(getChainNames(), prop, receiver);
105
+ },
106
+ has(_target, prop) {
107
+ return prop in getChainNames();
108
+ },
109
+ ownKeys() {
110
+ return Reflect.ownKeys(getChainNames());
111
+ },
112
+ getOwnPropertyDescriptor(_target, prop) {
113
+ return Object.getOwnPropertyDescriptor(getChainNames(), prop);
114
+ },
115
+ set(_target, _prop, _value) {
116
+ throw new TypeError('CHAIN_NAMES is read-only');
117
+ },
118
+ deleteProperty(_target, _prop) {
119
+ throw new TypeError('CHAIN_NAMES is read-only');
120
+ },
121
+ });
122
+ let _assetByCaip19Map = null;
62
123
  /**
63
- * Get asset by CAIP-19 identifier from the full dataset
124
+ * Get asset by CAIP-19 identifier from the full dataset.
125
+ * O(1) after first call; returns the same reference across calls
126
+ * so consumers can rely on referential equality.
64
127
  */
65
128
  export function getAssetByCaip19(caip19) {
66
- const assets = filteredData.assets;
67
- const asset = assets[caip19];
68
- if (!asset)
69
- return undefined;
70
- return {
71
- ...asset,
72
- gradient: asset.gradient.length >= 2
73
- ? [asset.gradient[0], asset.gradient[1]]
74
- : ['000000', '000000'],
75
- caip19: asset.caip19,
76
- };
129
+ if (!_assetByCaip19Map) {
130
+ _assetByCaip19Map = new Map();
131
+ for (const asset of getAllAssetsArray()) {
132
+ _assetByCaip19Map.set(asset.caip19, asset);
133
+ }
134
+ }
135
+ return _assetByCaip19Map.get(caip19);
77
136
  }
78
137
  /**
79
- * Get chain info by CAIP-2 identifier
138
+ * Get chain info by CAIP-2 identifier.
139
+ * Uses the underlying object as a cache; record lookup is already O(1).
80
140
  */
81
141
  export function getChainByCaip2(caip2) {
82
- const chains = filteredData.chains;
142
+ const chains = getFilteredData().chains;
83
143
  return chains[caip2];
84
144
  }
145
+ // Pre-indexed map of EVM chain ID → assets. Built lazily on first `getAssetsByChain`
146
+ // call; avoids re-scanning the full asset array (~thousands of entries) per call.
147
+ const EIP155_CHAIN_ID_REGEX = /^eip155:(\d+)\//;
148
+ let _assetsByEvmChainCache = null;
85
149
  /**
86
150
  * Get all assets filtered by chain ID (for ingress/egress)
87
151
  */
88
152
  export function getAssetsByChain(chainId, ingress = true) {
89
- return getAllAssetsArray().filter(asset => {
90
- const match = asset.caip19.match(/^eip155:(\d+)\//);
91
- if (!match)
92
- return false;
93
- return match[1] === chainId;
94
- });
153
+ if (!_assetsByEvmChainCache) {
154
+ const cache = new Map();
155
+ for (const asset of getAllAssetsArray()) {
156
+ const match = EIP155_CHAIN_ID_REGEX.exec(asset.caip19);
157
+ if (!match)
158
+ continue;
159
+ const key = match[1];
160
+ let bucket = cache.get(key);
161
+ if (!bucket) {
162
+ bucket = [];
163
+ cache.set(key, bucket);
164
+ }
165
+ bucket.push(asset);
166
+ }
167
+ _assetsByEvmChainCache = cache;
168
+ }
169
+ return _assetsByEvmChainCache.get(chainId) ?? [];
95
170
  }
96
171
  /**
97
172
  * Get chain name from chain ID
98
173
  */
99
174
  export function getChainName(chainId) {
100
175
  const chainInfo = getChainByCaip2(`eip155:${chainId}`);
101
- return chainInfo?.name || CHAIN_NAMES[chainId] || `Chain ${chainId}`;
176
+ // Call getChainNames() directly skips the Proxy trap on the public CHAIN_NAMES export.
177
+ return chainInfo?.name || getChainNames()[chainId] || `Chain ${chainId}`;
102
178
  }
179
+ let _lowerSearchIndex = null;
103
180
  /**
104
181
  * Search assets by symbol, name, or CAIP-19
105
182
  */
106
183
  export function searchAssets(query) {
107
184
  const lowerQuery = query.toLowerCase();
108
- return getAllAssetsArray().filter(asset => asset.symbol.toLowerCase().includes(lowerQuery) ||
109
- asset.name.toLowerCase().includes(lowerQuery) ||
110
- asset.caip19.toLowerCase().includes(lowerQuery));
185
+ if (!_lowerSearchIndex) {
186
+ const all = getAllAssetsArray();
187
+ const index = new Array(all.length);
188
+ for (let i = 0; i < all.length; i++) {
189
+ const a = all[i];
190
+ index[i] = {
191
+ asset: a,
192
+ lowerSymbol: a.symbol.toLowerCase(),
193
+ lowerName: a.name.toLowerCase(),
194
+ lowerCaip19: a.caip19.toLowerCase(),
195
+ };
196
+ }
197
+ _lowerSearchIndex = index;
198
+ }
199
+ const result = [];
200
+ for (const entry of _lowerSearchIndex) {
201
+ if (entry.lowerSymbol.includes(lowerQuery) ||
202
+ entry.lowerName.includes(lowerQuery) ||
203
+ entry.lowerCaip19.includes(lowerQuery)) {
204
+ result.push(entry.asset);
205
+ }
206
+ }
207
+ return result;
111
208
  }
package/dist/bridge.js CHANGED
@@ -610,9 +610,14 @@ async function solveDebridgeUsdcAmount(srcChainId, srcToken, srcAmount, userAddr
610
610
  }),
611
611
  });
612
612
  // Calculate price impact
613
+ // Use originApproximateUsdValue when available to exclude prepended operating expenses
614
+ // from the impact calculation. When prependOperatingExpenses=true, approximateUsdValue
615
+ // includes opex which inflates the apparent impact (e.g. 5.2% vs real 0.9%).
616
+ const usdInForImpact = quote.estimation.srcChainTokenIn.originApproximateUsdValue
617
+ ?? quote.estimation.srcChainTokenIn.approximateUsdValue;
613
618
  const usdIn = quote.estimation.srcChainTokenIn.approximateUsdValue;
614
619
  const usdOut = quote.estimation.dstChainTokenOut.approximateUsdValue;
615
- const impactPercent = 100 * (1 - usdOut / usdIn);
620
+ const impactPercent = 100 * (1 - usdOut / usdInForImpact);
616
621
  if (impactPercent > maxImpactPercent) {
617
622
  throw new Error(`Price impact too high: ${impactPercent.toFixed(2)}%`);
618
623
  }
@@ -696,9 +701,12 @@ async function solveDebridgeSingleChainUsdcAmount(chainId, srcToken, srcAmount,
696
701
  }),
697
702
  });
698
703
  // Calculate price impact
704
+ // Use originApproximateUsdValue when available to exclude prepended operating expenses
705
+ const usdInForImpact = quote.estimation.srcChainTokenIn.originApproximateUsdValue
706
+ ?? quote.estimation.srcChainTokenIn.approximateUsdValue;
699
707
  const usdIn = quote.estimation.srcChainTokenIn.approximateUsdValue;
700
708
  const usdOut = quote.estimation.dstChainTokenOut.approximateUsdValue;
701
- const impactPercent = 100 * (1 - usdOut / usdIn);
709
+ const impactPercent = 100 * (1 - usdOut / usdInForImpact);
702
710
  if (impactPercent > maxImpactPercent) {
703
711
  throw new Error(`Price impact too high: ${impactPercent.toFixed(2)}%`);
704
712
  }
package/dist/caip19.d.ts CHANGED
@@ -73,7 +73,8 @@ export interface ParsedTronCaip19 {
73
73
  tokenAddress: string | null;
74
74
  }
75
75
  /**
76
- * Parse a CAIP-19 string into its components
76
+ * Parse a CAIP-19 string into its components.
77
+ * Memoized — subsequent calls with the same input return the cached result.
77
78
  * @param caip19 - CAIP-19 formatted string
78
79
  * @returns Parsed components or null if invalid
79
80
  * @example
@@ -82,7 +83,6 @@ export interface ParsedTronCaip19 {
82
83
  * parseCaip19('solana:1/token:0x0000000000000000000000000000000000000000') // { chainNamespace: 'solana', chainId: '1', assetNamespace: 'token', assetReference: '0x0000000000000000000000000000000000000000' }
83
84
  * parseCaip19('solana:1/slip44:501') // { chainNamespace: 'solana', chainId: '1', assetNamespace: 'slip44', assetReference: '501' }
84
85
  * parseCaip19('bip122:0/bip44:60') // { chainNamespace: 'bip122', chainId: '0', assetNamespace: 'bip44', assetReference: '60' }
85
- * parseCaip19('bip122:0/bip44:60') // { chainNamespace: 'bip122', chainId: '0', assetNamespace: 'bip44', assetReference: '60' }
86
86
  */
87
87
  export declare function parseCaip19(caip19: string): ParsedCaip19 | null;
88
88
  /**
package/dist/caip19.js CHANGED
@@ -40,8 +40,13 @@ export var Caip19Namespace;
40
40
  // ============================================================================
41
41
  // Parsing Functions
42
42
  // ============================================================================
43
+ // Memoized parse results. Asset catalog is bounded (few thousand), so the Map
44
+ // stays small in practice; invalid inputs are cached as `null` to avoid re-running
45
+ // the regex on repeated bad inputs (e.g. empty strings during render-time guards).
46
+ const _parseCaip19Cache = new Map();
43
47
  /**
44
- * Parse a CAIP-19 string into its components
48
+ * Parse a CAIP-19 string into its components.
49
+ * Memoized — subsequent calls with the same input return the cached result.
45
50
  * @param caip19 - CAIP-19 formatted string
46
51
  * @returns Parsed components or null if invalid
47
52
  * @example
@@ -50,19 +55,25 @@ export var Caip19Namespace;
50
55
  * parseCaip19('solana:1/token:0x0000000000000000000000000000000000000000') // { chainNamespace: 'solana', chainId: '1', assetNamespace: 'token', assetReference: '0x0000000000000000000000000000000000000000' }
51
56
  * parseCaip19('solana:1/slip44:501') // { chainNamespace: 'solana', chainId: '1', assetNamespace: 'slip44', assetReference: '501' }
52
57
  * parseCaip19('bip122:0/bip44:60') // { chainNamespace: 'bip122', chainId: '0', assetNamespace: 'bip44', assetReference: '60' }
53
- * parseCaip19('bip122:0/bip44:60') // { chainNamespace: 'bip122', chainId: '0', assetNamespace: 'bip44', assetReference: '60' }
54
58
  */
55
59
  export function parseCaip19(caip19) {
60
+ const cached = _parseCaip19Cache.get(caip19);
61
+ if (cached !== undefined)
62
+ return cached;
56
63
  // Spec: 'namespace:chainId/assetNamespace:assetReference'
57
64
  const match = CAIP19_REGEX.exec(caip19);
58
- if (!match)
65
+ if (!match) {
66
+ _parseCaip19Cache.set(caip19, null);
59
67
  return null;
60
- return {
68
+ }
69
+ const result = {
61
70
  chainNamespace: match[1],
62
71
  chainId: match[2],
63
72
  assetNamespace: match[3],
64
73
  assetReference: match[4],
65
74
  };
75
+ _parseCaip19Cache.set(caip19, result);
76
+ return result;
66
77
  }
67
78
  /**
68
79
  * Parse an EVM CAIP-19 string (eip155:chainId/erc20:address or eip155:chainId/slip44:cointype)
package/dist/chain.js CHANGED
@@ -61,6 +61,12 @@ export async function ensureChain(chainId, walletClient, connector, options) {
61
61
  console.log('[ensureChain] recreating walletClient from provider ...', { switchSucceeded });
62
62
  const provider = await connector.getProvider();
63
63
  const chain = getChainById(chainId);
64
+ if (!chain) {
65
+ throw new Error(`Unsupported chain ID: ${chainId}. Please ensure the chain is supported.`);
66
+ }
67
+ if (!walletClient.account) {
68
+ throw new Error('walletClient.account is undefined. A connected account is required to sign transactions.');
69
+ }
64
70
  const newClient = createWalletClient({
65
71
  account: walletClient.account,
66
72
  chain,
@@ -94,13 +94,14 @@ function normalizeTokenAddressForQuote(chainId, token) {
94
94
  * Calculate metrics from a Relay.link quote response
95
95
  */
96
96
  export function calculateRelayMetrics(relayQuote) {
97
- const usdIn = relayQuote.details.currencyIn.amountUsd;
98
- const usdOut = relayQuote.details.currencyOut.amountUsd;
97
+ const { details, fees } = relayQuote;
98
+ const usdIn = details.currencyIn.amountUsd;
99
+ const usdOut = details.currencyOut.amountUsd;
99
100
  return {
100
101
  retention: BigNumber(usdOut).div(usdIn).toNumber(),
101
- feeUsd: Number(relayQuote.fees.relayerGas?.amountUsd || relayQuote.fees.gas?.amountUsd || '0'),
102
- slippage: Number(relayQuote.details.totalImpact?.percent || '0') * 100,
103
- time: relayQuote.details.timeEstimate || 300,
102
+ feeUsd: Number(fees.relayerGas?.amountUsd || fees.gas?.amountUsd || '0'),
103
+ slippage: Number(details.totalImpact?.percent || '0') * 100,
104
+ time: details.timeEstimate || 300,
104
105
  txCount: relayQuote.steps?.length || 1,
105
106
  };
106
107
  }
@@ -94,11 +94,10 @@ async function authenticate(client, signer, siweDomain, onStatus) {
94
94
  return { entropy, secretToken: authResponse.secretToken };
95
95
  }
96
96
  // ============================================================================
97
- // Step 2: Calculate Bridge Amount & Get Quote
97
+ // Step 2a: Calculate Bridge Amount (independent can run in parallel with auth)
98
98
  // ============================================================================
99
- async function calculateBridgeAndQuote(client, evmAddress, solanaAddress, sourceAmountUnits, destinationAsset, recipientAddress, facilitatorGroup, maxImpactPercent, onStatus) {
99
+ async function calculateBridgeAmount(client, evmAddress, solanaAddress, sourceAmountUnits, maxImpactPercent, onStatus) {
100
100
  onStatus?.({ type: 'info', message: 'Calculating optimal bridge amount...' });
101
- // Create phony deposit calldata for relay quote
102
101
  const depositorAddress = client.s0xDepositorAddress;
103
102
  const phonyCalldata = createPhonyDepositCalldata(evmAddress);
104
103
  const bridgeResult = await solveOptimalUsdcAmount(N_RELAY_CHAIN_ID_SOLANA, SB58_ADDR_SOL_PROGRAM_SYSTEM, sourceAmountUnits, solanaAddress, phonyCalldata, maxImpactPercent, depositorAddress, evmAddress);
@@ -108,11 +107,19 @@ async function calculateBridgeAndQuote(client, evmAddress, solanaAddress, source
108
107
  message: `Bridge will provide ${usdcHuman} USDC`,
109
108
  data: { provider: bridgeResult.provider, usdcAmount: bridgeResult.usdcAmountOut.toString() },
110
109
  });
110
+ return bridgeResult;
111
+ }
112
+ // ============================================================================
113
+ // Step 2b: Request Quote (needs facilitatorGroup + bridgeResult)
114
+ // ============================================================================
115
+ async function requestQuote(client, evmAddress, destinationAsset, recipientAddress, facilitatorGroup, bridgeResult, onStatus) {
111
116
  onStatus?.({ type: 'info', message: 'Requesting quote from SilentSwap...' });
112
- // Export facilitator group keys
113
- const viewer = await facilitatorGroup.viewer();
117
+ // Export facilitator group keys — viewer() and exportPublicKeys() are independent RPCs.
118
+ const [viewer, groupPublicKeys] = await Promise.all([
119
+ facilitatorGroup.viewer(),
120
+ facilitatorGroup.exportPublicKeys(1, [...PublicKeyArgGroups.GENERIC]),
121
+ ]);
114
122
  const { publicKeyBytes: pk65_viewer } = viewer.exportPublicKey('*', FacilitatorKeyType.SECP256K1);
115
- const groupPublicKeys = await facilitatorGroup.exportPublicKeys(1, [...PublicKeyArgGroups.GENERIC]);
116
123
  // Build quote request
117
124
  const [quoteError, quoteResponse] = await client.quote({
118
125
  signer: evmAddress,
@@ -135,7 +142,7 @@ async function calculateBridgeAndQuote(client, evmAddress, solanaAddress, source
135
142
  message: 'Quote received',
136
143
  data: { quoteId: quoteResponse.quoteId, deposit: quoteResponse.quote?.deposit },
137
144
  });
138
- return { quoteResponse, bridgeResult };
145
+ return quoteResponse;
139
146
  }
140
147
  // ============================================================================
141
148
  // Step 3: Place Order
@@ -335,14 +342,17 @@ async function executeDeposit(solanaAccount, solanaConnection, evmAddress, order
335
342
  * Poll relay.link bridge status until completion or timeout
336
343
  */
337
344
  async function pollBridgeStatus(requestId, onStatus) {
338
- let attempts = 0;
339
- const maxAttempts = 180; // ~6 minutes at 2s intervals
340
- while (attempts < maxAttempts) {
345
+ const maxDurationMs = 6 * 60 * 1000; // 6 minutes total timeout
346
+ const startTime = Date.now();
347
+ let delay = 2000; // Start at 2s
348
+ let attempt = 0;
349
+ while (Date.now() - startTime < maxDurationMs) {
341
350
  const status = await getRelayStatus(requestId);
351
+ attempt++;
342
352
  onStatus?.({
343
353
  type: 'info',
344
354
  message: `Bridge status: ${status.status}`,
345
- data: { requestId, status: status.status, attempt: attempts + 1 },
355
+ data: { requestId, status: status.status, attempt },
346
356
  });
347
357
  if (status.status === 'success') {
348
358
  return;
@@ -350,10 +360,11 @@ async function pollBridgeStatus(requestId, onStatus) {
350
360
  if (status.status === 'failed' || status.status === 'refund') {
351
361
  throw new Error(`Bridge failed with status: ${status.status}${status.details ? ` - ${status.details}` : ''}`);
352
362
  }
353
- await new Promise((r) => setTimeout(r, 2000));
354
- attempts++;
363
+ await new Promise((r) => setTimeout(r, delay));
364
+ // Exponential backoff: 2s → 4s → 8s, capped at 15s
365
+ delay = Math.min(delay * 2, 15_000);
355
366
  }
356
- throw new Error(`Bridge status polling timeout after ${maxAttempts} attempts`);
367
+ throw new Error(`Bridge status polling timeout after ${Math.round((Date.now() - startTime) / 1000)}s`);
357
368
  }
358
369
  // ============================================================================
359
370
  // Depositor ABI (depositProxy2)
@@ -460,14 +471,21 @@ export async function executeSolanaSwap(config) {
460
471
  // Create EVM signer adapter
461
472
  const evmSigner = createEvmSignerFromAccount(accounts.evm);
462
473
  onStatus?.({ type: 'info', message: 'Starting Solana swap...' });
463
- // Step 1: Authenticate
464
- const { entropy } = await authenticate(client, evmSigner, siweDomain, onStatus);
465
- // Step 2: Create facilitator group
474
+ // Steps 2a, 2b run in parallel first — they are cheap/automatic and can fail
475
+ // without user interaction. Only after they succeed do we prompt the user for
476
+ // the SIWE signature, so we never ask the user to sign for a doomed operation.
477
+ const [depositCount, bridgeResult] = await Promise.all([
478
+ queryDepositCount(accounts.evm.address, client.s0xGatewayAddress),
479
+ calculateBridgeAmount(client, accounts.evm.address, accounts.solana.publicKey, sourceAmountUnits, maxImpactPercent, onStatus),
480
+ ]);
481
+ // Step 1: Authenticate (prompts user for SIWE signature) — sequenced after
482
+ // the above so a failing RPC/quote doesn't waste a user signature.
483
+ const authResult = await authenticate(client, evmSigner, siweDomain, onStatus);
484
+ // Step 2c: Create facilitator group (needs entropy + depositCount from above)
466
485
  onStatus?.({ type: 'info', message: 'Creating facilitator group...' });
467
- const depositCount = await queryDepositCount(accounts.evm.address, client.s0xGatewayAddress);
468
- const facilitatorGroup = await createHdFacilitatorGroupFromEntropy(hexToBytes(entropy), depositCount);
469
- // Step 3: Calculate bridge and get quote
470
- const { quoteResponse, bridgeResult } = await calculateBridgeAndQuote(client, accounts.evm.address, accounts.solana.publicKey, sourceAmountUnits, destinationAsset, recipientAddress, facilitatorGroup, maxImpactPercent, onStatus);
486
+ const facilitatorGroup = await createHdFacilitatorGroupFromEntropy(hexToBytes(authResult.entropy), depositCount);
487
+ // Step 3: Request quote (needs facilitatorGroup + bridgeResult from above)
488
+ const quoteResponse = await requestQuote(client, accounts.evm.address, destinationAsset, recipientAddress, facilitatorGroup, bridgeResult, onStatus);
471
489
  // Create encrypt args for facilitator key export
472
490
  const encryptArgs = {
473
491
  proxyPublicKey: client.proxyPublicKey,
@@ -500,19 +518,22 @@ export async function executeSolanaSwap(config) {
500
518
  });
501
519
  }
502
520
  }
503
- // Derive viewing auth (viewer EVM address as Base58) for tracking, refund, and recovery
504
- const viewer = await facilitatorGroup.viewer();
521
+ // Derive viewing auth + recovery data in parallel viewer(), exportPublicKeys(),
522
+ // and exportSecretMnemonicFromEntropy() are independent once authResult is ready.
523
+ const [viewer, groupPublicKeysForRecovery, mnemonicResult] = await Promise.all([
524
+ facilitatorGroup.viewer(),
525
+ facilitatorGroup.exportPublicKeys(1, [...PublicKeyArgGroups.GENERIC]),
526
+ exportSecretMnemonicFromEntropy(hexToBytes(authResult.entropy)),
527
+ ]);
505
528
  const viewerEvmSigner = await viewer.evmSigner();
506
529
  const viewingAuth = hexToBase58(viewerEvmSigner.address);
507
- // Build recovery data (mnemonic + paths) for refund/recovery
508
- const groupPublicKeysForRecovery = await facilitatorGroup.exportPublicKeys(1, [...PublicKeyArgGroups.GENERIC]);
509
530
  const recoveryPaths = [];
510
531
  for (const pk of groupPublicKeysForRecovery[0] ?? []) {
511
532
  if (pk.coinType === '*')
512
533
  continue;
513
534
  recoveryPaths.push(`m/44'/${pk.coinType}'/${depositCount}'/0/0`);
514
535
  }
515
- const mnemonic = (await exportSecretMnemonicFromEntropy(hexToBytes(entropy))).toString();
536
+ const mnemonic = mnemonicResult.toString();
516
537
  const recoveryData = { mnemonic, recoveryPaths };
517
538
  return {
518
539
  orderId,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@silentswap/sdk",
3
3
  "type": "module",
4
- "version": "0.1.56",
4
+ "version": "0.1.58",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
7
7
  "files": [