@silentswap/sdk 0.1.57 → 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 +6 -2
- package/dist/assets.js +161 -64
- package/dist/caip19.d.ts +2 -2
- package/dist/caip19.js +15 -4
- package/dist/chain.js +6 -0
- package/dist/quote-utils.js +6 -5
- package/dist/swap-executor.js +47 -26
- package/package.json +1 -1
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
|
|
7
|
+
return getFilteredData();
|
|
5
8
|
}
|
|
6
9
|
// Get all assets from the filtered data
|
|
7
10
|
export function getAllAssets() {
|
|
8
|
-
return
|
|
11
|
+
return getFilteredData().assets;
|
|
9
12
|
}
|
|
10
13
|
// Get all chains from the filtered data
|
|
11
14
|
export function getAllChains() {
|
|
12
|
-
return
|
|
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
|
-
|
|
17
|
-
|
|
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
|
|
33
|
+
// Common CAIP-19 identifiers for popular tokens
|
|
26
34
|
// This matches the popular tokens list in the Svelte UI (PopularTokensArea.svelte)
|
|
27
|
-
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
if (
|
|
58
|
-
return
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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 =
|
|
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
|
-
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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/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
|
-
|
|
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,
|
package/dist/quote-utils.js
CHANGED
|
@@ -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
|
|
98
|
-
const
|
|
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(
|
|
102
|
-
slippage: Number(
|
|
103
|
-
time:
|
|
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
|
}
|
package/dist/swap-executor.js
CHANGED
|
@@ -94,11 +94,10 @@ async function authenticate(client, signer, siweDomain, onStatus) {
|
|
|
94
94
|
return { entropy, secretToken: authResponse.secretToken };
|
|
95
95
|
}
|
|
96
96
|
// ============================================================================
|
|
97
|
-
// Step
|
|
97
|
+
// Step 2a: Calculate Bridge Amount (independent — can run in parallel with auth)
|
|
98
98
|
// ============================================================================
|
|
99
|
-
async function
|
|
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
|
|
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
|
|
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
|
-
|
|
339
|
-
const
|
|
340
|
-
|
|
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
|
|
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,
|
|
354
|
-
|
|
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 ${
|
|
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
|
-
//
|
|
464
|
-
|
|
465
|
-
//
|
|
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
|
|
468
|
-
|
|
469
|
-
|
|
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
|
|
504
|
-
|
|
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 =
|
|
536
|
+
const mnemonic = mnemonicResult.toString();
|
|
516
537
|
const recoveryData = { mnemonic, recoveryPaths };
|
|
517
538
|
return {
|
|
518
539
|
orderId,
|