@swapkit/core 1.0.0-rc.12 → 1.0.0-rc.121
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.js +7324 -0
- package/dist/index.js.map +41 -0
- package/package.json +33 -47
- package/src/__tests__/helpers.test.ts +53 -0
- package/src/aggregator/contracts/avaxGeneric.ts +50 -50
- package/src/aggregator/contracts/avaxWoofi.ts +80 -80
- package/src/aggregator/contracts/bscGeneric.ts +59 -59
- package/src/aggregator/contracts/ethGeneric.ts +50 -50
- package/src/aggregator/contracts/index.ts +30 -28
- package/src/aggregator/contracts/pancakeV2.ts +80 -80
- package/src/aggregator/contracts/pangolin.ts +65 -65
- package/src/aggregator/contracts/routers/index.ts +58 -0
- package/src/aggregator/contracts/routers/kyber.ts +402 -0
- package/src/aggregator/contracts/routers/oneinch.ts +2188 -0
- package/src/aggregator/contracts/routers/pancakeswap.ts +340 -0
- package/src/aggregator/contracts/routers/pangolin.ts +340 -0
- package/src/aggregator/contracts/routers/sushiswap.ts +340 -0
- package/src/aggregator/contracts/routers/traderJoe.ts +340 -0
- package/src/aggregator/contracts/routers/uniswapv2.ts +340 -0
- package/src/aggregator/contracts/routers/uniswapv3.ts +254 -0
- package/src/aggregator/contracts/routers/woofi.ts +171 -0
- package/src/aggregator/contracts/sushiswap.ts +65 -65
- package/src/aggregator/contracts/traderJoe.ts +65 -65
- package/src/aggregator/contracts/uniswapV2.ts +65 -65
- package/src/aggregator/contracts/uniswapV2Leg.ts +70 -70
- package/src/aggregator/contracts/uniswapV3_100.ts +70 -70
- package/src/aggregator/contracts/uniswapV3_10000.ts +70 -70
- package/src/aggregator/contracts/uniswapV3_3000.ts +70 -70
- package/src/aggregator/contracts/uniswapV3_500.ts +70 -70
- package/src/aggregator/getSwapParams.ts +12 -12
- package/src/client/index.ts +206 -646
- package/src/client/old.ts +854 -0
- package/src/helpers/explorerUrls.ts +38 -0
- package/src/index.ts +6 -4
- package/src/types.ts +149 -0
- package/LICENSE +0 -201
- package/dist/index-9e36735e.cjs +0 -1
- package/dist/index-cf1865cd.js +0 -649
- package/dist/index.cjs +0 -2
- package/dist/index.d.ts +0 -216
- package/dist/index.es.js +0 -3818
- package/src/client/__tests__/helpers.test.ts +0 -69
- package/src/client/explorerUrls.ts +0 -62
- package/src/client/thornode.ts +0 -31
- package/src/client/types.ts +0 -103
package/src/client/index.ts
CHANGED
|
@@ -1,689 +1,249 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { AssetValue, SwapKitError } from "@swapkit/helpers";
|
|
2
|
+
import { Chain } from "@swapkit/types";
|
|
2
3
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
getMinAmountByChain,
|
|
7
|
-
SwapKitError,
|
|
8
|
-
SwapKitNumber,
|
|
9
|
-
} from '@swapkit/helpers';
|
|
10
|
-
import type { CosmosLikeToolbox } from '@swapkit/toolbox-cosmos';
|
|
11
|
-
import type { AVAXToolbox, BSCToolbox, ETHToolbox, EVMToolbox } from '@swapkit/toolbox-evm';
|
|
12
|
-
import type { UTXOToolbox } from '@swapkit/toolbox-utxo';
|
|
4
|
+
getExplorerAddressUrl as getAddressUrl,
|
|
5
|
+
getExplorerTxUrl as getTxUrl,
|
|
6
|
+
} from "../helpers/explorerUrls.ts";
|
|
13
7
|
import type {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
WalletOption,
|
|
19
|
-
} from '@swapkit/types';
|
|
20
|
-
import {
|
|
21
|
-
AGG_SWAP,
|
|
22
|
-
Chain,
|
|
23
|
-
ChainToChainId,
|
|
24
|
-
FeeOption,
|
|
25
|
-
MemoType,
|
|
26
|
-
SWAP_IN,
|
|
27
|
-
SWAP_OUT,
|
|
28
|
-
TCAvalancheDepositABI,
|
|
29
|
-
TCBscDepositABI,
|
|
30
|
-
TCEthereumVaultAbi,
|
|
31
|
-
} from '@swapkit/types';
|
|
32
|
-
|
|
33
|
-
import type { AGG_CONTRACT_ADDRESS } from '../aggregator/contracts/index.ts';
|
|
34
|
-
import { lowercasedContractAbiMapping } from '../aggregator/contracts/index.ts';
|
|
35
|
-
import { getSwapInParams } from '../aggregator/getSwapParams.ts';
|
|
36
|
-
|
|
37
|
-
import { getExplorerAddressUrl, getExplorerTxUrl } from './explorerUrls.ts';
|
|
38
|
-
import { getInboundData, getMimirData } from './thornode.ts';
|
|
39
|
-
import type {
|
|
40
|
-
CoreTxParams,
|
|
41
|
-
EVMWallet,
|
|
42
|
-
SwapParams,
|
|
43
|
-
ThorchainWallet,
|
|
44
|
-
Wallet,
|
|
45
|
-
WalletMethods,
|
|
46
|
-
} from './types.ts';
|
|
47
|
-
|
|
48
|
-
const getEmptyWalletStructure = () =>
|
|
49
|
-
(Object.values(Chain) as Chain[]).reduce(
|
|
50
|
-
(acc, chain) => {
|
|
51
|
-
acc[chain] = null;
|
|
52
|
-
return acc;
|
|
53
|
-
},
|
|
54
|
-
{} as Record<Chain, null>,
|
|
55
|
-
);
|
|
56
|
-
|
|
57
|
-
export class SwapKitCore<T = ''> {
|
|
58
|
-
public connectedChains: Wallet = getEmptyWalletStructure();
|
|
59
|
-
public connectedWallets: WalletMethods = getEmptyWalletStructure();
|
|
60
|
-
public readonly stagenet: boolean = false;
|
|
61
|
-
|
|
62
|
-
constructor({ stagenet }: { stagenet?: boolean } | undefined = {}) {
|
|
63
|
-
this.stagenet = !!stagenet;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
getAddress = (chain: Chain) => this.connectedChains[chain]?.address || '';
|
|
67
|
-
getExplorerTxUrl = (chain: Chain, txHash: string) => getExplorerTxUrl({ chain, txHash });
|
|
68
|
-
getWallet = <T extends Chain>(chain: Chain) => this.connectedWallets[chain] as WalletMethods[T];
|
|
69
|
-
getExplorerAddressUrl = (chain: Chain, address: string) =>
|
|
70
|
-
getExplorerAddressUrl({ chain, address });
|
|
71
|
-
getBalance = async (chain: Chain, potentialScamFilter?: boolean) => {
|
|
72
|
-
const wallet = await this.getWalletByChain(chain, potentialScamFilter);
|
|
73
|
-
|
|
74
|
-
return wallet?.balance || [];
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
swap = async ({ streamSwap, recipient, route, feeOptionKey }: SwapParams) => {
|
|
78
|
-
const { quoteMode } = route.meta;
|
|
79
|
-
const evmChain = quoteMode.startsWith('ERC20-')
|
|
80
|
-
? Chain.Ethereum
|
|
81
|
-
: quoteMode.startsWith('ARC20-')
|
|
82
|
-
? Chain.Avalanche
|
|
83
|
-
: Chain.BinanceSmartChain;
|
|
84
|
-
|
|
85
|
-
if (!route.complete) throw new SwapKitError('core_swap_route_not_complete');
|
|
86
|
-
|
|
87
|
-
try {
|
|
88
|
-
if (AGG_SWAP.includes(quoteMode)) {
|
|
89
|
-
const walletMethods = this.connectedWallets[evmChain];
|
|
90
|
-
if (!walletMethods?.sendTransaction) {
|
|
91
|
-
throw new SwapKitError('core_wallet_connection_not_found');
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const transaction = streamSwap ? route?.streamingSwap?.transaction : route?.transaction;
|
|
95
|
-
if (!transaction) throw new SwapKitError('core_swap_route_transaction_not_found');
|
|
96
|
-
|
|
97
|
-
const { isHexString, parseUnits } = await import('ethers');
|
|
98
|
-
const { data, from, to, value } = route.transaction;
|
|
99
|
-
|
|
100
|
-
const params = {
|
|
101
|
-
data,
|
|
102
|
-
from,
|
|
103
|
-
to: to.toLowerCase(),
|
|
104
|
-
chainId: BigInt(ChainToChainId[evmChain]),
|
|
105
|
-
value: value
|
|
106
|
-
? new SwapKitNumber({
|
|
107
|
-
value: !isHexString(value) ? parseUnits(value, 'wei').toString(16) : value,
|
|
108
|
-
}).baseValueBigInt
|
|
109
|
-
: 0n,
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
return walletMethods.sendTransaction(params, feeOptionKey) as Promise<string>;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (SWAP_OUT.includes(quoteMode)) {
|
|
116
|
-
if (!route.calldata.fromAsset) throw new SwapKitError('core_swap_asset_not_recognized');
|
|
117
|
-
const asset = await AssetValue.fromString(route.calldata.fromAsset);
|
|
118
|
-
if (!asset) throw new SwapKitError('core_swap_asset_not_recognized');
|
|
119
|
-
|
|
120
|
-
const { address: recipient } = await this.#getInboundDataByChain(asset.chain);
|
|
121
|
-
const {
|
|
122
|
-
contract: router,
|
|
123
|
-
calldata: { amountIn, memo, memoStreamingSwap },
|
|
124
|
-
} = route;
|
|
125
|
-
|
|
126
|
-
const assetValue = asset.add(SwapKitNumber.fromBigInt(BigInt(amountIn), asset.decimal));
|
|
127
|
-
const swapMemo = (streamSwap ? memoStreamingSwap || memo : memo) as string;
|
|
128
|
-
|
|
129
|
-
return this.deposit({ assetValue, memo: swapMemo, feeOptionKey, router, recipient });
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
if (SWAP_IN.includes(quoteMode)) {
|
|
133
|
-
const { calldata, contract: contractAddress } = route;
|
|
134
|
-
if (!contractAddress) throw new SwapKitError('core_swap_contract_not_found');
|
|
135
|
-
|
|
136
|
-
const walletMethods = this.connectedWallets[evmChain];
|
|
137
|
-
const from = this.getAddress(evmChain);
|
|
138
|
-
if (!walletMethods?.sendTransaction || !from) {
|
|
139
|
-
throw new SwapKitError('core_wallet_connection_not_found');
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const { getProvider, toChecksumAddress } = await import('@swapkit/toolbox-evm');
|
|
143
|
-
const provider = getProvider(evmChain);
|
|
144
|
-
const abi = lowercasedContractAbiMapping[contractAddress.toLowerCase()];
|
|
145
|
-
|
|
146
|
-
if (!abi) throw new SwapKitError('core_swap_contract_not_supported', { contractAddress });
|
|
147
|
-
|
|
148
|
-
const contract = await walletMethods.createContract?.(contractAddress, abi, provider);
|
|
149
|
-
|
|
150
|
-
// TODO: (@Towan) Contract evm methods should be generic
|
|
151
|
-
// @ts-expect-error
|
|
152
|
-
const tx = await contract.populateTransaction.swapIn?.(
|
|
153
|
-
...getSwapInParams({
|
|
154
|
-
streamSwap,
|
|
155
|
-
toChecksumAddress,
|
|
156
|
-
contractAddress: contractAddress as AGG_CONTRACT_ADDRESS,
|
|
157
|
-
recipient,
|
|
158
|
-
calldata,
|
|
159
|
-
}),
|
|
160
|
-
{ from },
|
|
161
|
-
);
|
|
162
|
-
|
|
163
|
-
return walletMethods.sendTransaction(tx, feeOptionKey) as Promise<string>;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
throw new SwapKitError('core_swap_quote_mode_not_supported', { quoteMode });
|
|
167
|
-
} catch (error) {
|
|
168
|
-
throw new SwapKitError('core_swap_transaction_error', error);
|
|
169
|
-
}
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
getWalletByChain = async (chain: Chain, potentialScamFilter?: boolean) => {
|
|
173
|
-
const address = this.getAddress(chain);
|
|
174
|
-
if (!address) return null;
|
|
175
|
-
|
|
176
|
-
const balance = (await this.getWallet(chain)?.getBalance(address, potentialScamFilter)) ?? [
|
|
177
|
-
AssetValue.fromChainOrSignature(chain),
|
|
178
|
-
];
|
|
179
|
-
|
|
180
|
-
this.connectedChains[chain] = {
|
|
181
|
-
address,
|
|
182
|
-
balance,
|
|
183
|
-
walletType: this.connectedChains[chain]?.walletType as WalletOption,
|
|
184
|
-
};
|
|
185
|
-
|
|
186
|
-
return { ...this.connectedChains[chain] };
|
|
187
|
-
};
|
|
188
|
-
|
|
189
|
-
approveAssetValue = (assetValue: AssetValue, contractAddress?: string) =>
|
|
190
|
-
this.#approve({ assetValue, type: 'approve', contractAddress });
|
|
8
|
+
ChainWallet,
|
|
9
|
+
ConnectWalletParamsLocal as ConnectWalletParams,
|
|
10
|
+
SwapWithRouteParams,
|
|
11
|
+
} from "../types.ts";
|
|
191
12
|
|
|
192
|
-
|
|
193
|
-
this.#approve<boolean>({ assetValue, contractAddress, type: 'checkOnly' });
|
|
13
|
+
export type PluginName = "thorchain" | "chainflip" | "mayachain";
|
|
194
14
|
|
|
195
|
-
|
|
196
|
-
|
|
15
|
+
export enum ApproveMode {
|
|
16
|
+
Approve = "approve",
|
|
17
|
+
CheckOnly = "checkOnly",
|
|
18
|
+
}
|
|
197
19
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
20
|
+
export type ApproveReturnType<T extends ApproveMode> = T extends "checkOnly"
|
|
21
|
+
? Promise<boolean>
|
|
22
|
+
: Promise<string>;
|
|
23
|
+
|
|
24
|
+
type SwapKitPlugins = {
|
|
25
|
+
[K in PluginName]?: ProviderMethods;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type GenericSwapParams = {
|
|
29
|
+
buyAsset: AssetValue;
|
|
30
|
+
sellAsset: AssetValue;
|
|
31
|
+
recipient: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type SwapParams = (SwapWithRouteParams | GenericSwapParams) & {
|
|
35
|
+
provider?: {
|
|
36
|
+
name: PluginName;
|
|
37
|
+
config: Record<string, Todo>;
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type SwapKitReturnType = SwapKitPlugins & {
|
|
42
|
+
getAddress: (chain: Chain) => string;
|
|
43
|
+
getWallet: (chain: Chain) => ChainWallet<Chain> | undefined;
|
|
44
|
+
getWalletWithBalance: (
|
|
45
|
+
chain: Chain,
|
|
46
|
+
potentialScamFilter?: boolean,
|
|
47
|
+
) => Promise<ChainWallet<Chain>>;
|
|
48
|
+
getBalance: (chain: Chain, potentialScamFilter?: boolean) => AssetValue[];
|
|
49
|
+
getExplorerTxUrl: typeof getTxUrl;
|
|
50
|
+
getExplorerAddressUrl: typeof getAddressUrl;
|
|
51
|
+
swap: (params: SwapParams) => Promise<string>;
|
|
52
|
+
validateAddress: (params: { address: string; chain: Chain }) =>
|
|
53
|
+
| boolean
|
|
54
|
+
| Promise<boolean>
|
|
55
|
+
| undefined;
|
|
56
|
+
approveAssetValue: (assetValue: AssetValue, contractAddress: string) => boolean | Promise<string>;
|
|
57
|
+
isAssetValueApproved: (
|
|
58
|
+
assetValue: AssetValue,
|
|
59
|
+
contractAddress: string,
|
|
60
|
+
) => boolean | Promise<boolean>;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export type Wallets = { [K in Chain]?: ChainWallet<K> };
|
|
64
|
+
export type AvailableProviders<T> = T | { [K in PluginName]?: ProviderMethods };
|
|
65
|
+
export type ProviderMethods = {
|
|
66
|
+
swap: (swapParams: SwapParams) => Promise<string>;
|
|
67
|
+
[key: string]: Todo;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export type SwapKitPlugin = ({ wallets, stagenet }: { wallets: Wallets; stagenet?: boolean }) => {
|
|
71
|
+
name: PluginName;
|
|
72
|
+
methods: ProviderMethods;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export type SwapKitWallet = {
|
|
76
|
+
connectMethodName: string;
|
|
77
|
+
connect: (params: ConnectWalletParams) => (connectParams: Todo) => undefined | string;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export function SwapKit<
|
|
81
|
+
ExtendedProviders extends {},
|
|
82
|
+
ConnectWalletMethods = Record<string, ReturnType<SwapKitWallet["connect"]>>,
|
|
83
|
+
>({
|
|
84
|
+
stagenet,
|
|
85
|
+
wallets,
|
|
86
|
+
plugins,
|
|
87
|
+
config = {},
|
|
88
|
+
apis = {},
|
|
89
|
+
rpcUrls = {},
|
|
90
|
+
}: {
|
|
91
|
+
plugins: SwapKitPlugin[];
|
|
92
|
+
stagenet: boolean;
|
|
93
|
+
wallets: SwapKitWallet[];
|
|
94
|
+
config?: Record<string, Todo>;
|
|
95
|
+
apis: Record<string, Todo>;
|
|
96
|
+
rpcUrls: Record<string, Todo>;
|
|
97
|
+
}): SwapKitReturnType & ConnectWalletMethods & AvailableProviders<ExtendedProviders> {
|
|
98
|
+
const connectedWallets: Wallets = {};
|
|
99
|
+
const availablePlugins: AvailableProviders<ExtendedProviders> = {};
|
|
100
|
+
|
|
101
|
+
for (const plugin of plugins) {
|
|
102
|
+
const { name, methods } = plugin({ wallets: connectedWallets, stagenet });
|
|
103
|
+
|
|
104
|
+
availablePlugins[name] = methods;
|
|
105
|
+
}
|
|
201
106
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
107
|
+
const connectWalletMethods = wallets.reduce((acc, wallet) => {
|
|
108
|
+
// @ts-expect-error
|
|
109
|
+
acc[wallet.connectMethodName] = wallet.connect({
|
|
110
|
+
addChain,
|
|
111
|
+
config,
|
|
112
|
+
apis,
|
|
113
|
+
rpcUrls,
|
|
114
|
+
});
|
|
208
115
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
recipient,
|
|
212
|
-
router,
|
|
213
|
-
...rest
|
|
214
|
-
}: CoreTxParams & { router?: string }) => {
|
|
215
|
-
const { chain, symbol, ticker } = assetValue;
|
|
216
|
-
const walletInstance = this.connectedWallets[chain];
|
|
217
|
-
if (!walletInstance) throw new SwapKitError('core_wallet_connection_not_found');
|
|
218
|
-
|
|
219
|
-
const params = this.#prepareTxParams({ assetValue, recipient, router, ...rest });
|
|
220
|
-
|
|
221
|
-
try {
|
|
222
|
-
switch (chain) {
|
|
223
|
-
case Chain.THORChain: {
|
|
224
|
-
const wallet = walletInstance as ThorchainWallet;
|
|
225
|
-
return await (recipient === '' ? wallet.deposit(params) : wallet.transfer(params));
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
case Chain.Ethereum:
|
|
229
|
-
case Chain.BinanceSmartChain:
|
|
230
|
-
case Chain.Avalanche: {
|
|
231
|
-
const { getChecksumAddressFromAsset } = await import('@swapkit/toolbox-evm');
|
|
232
|
-
|
|
233
|
-
const abi =
|
|
234
|
-
chain === Chain.Avalanche
|
|
235
|
-
? TCAvalancheDepositABI
|
|
236
|
-
: chain === Chain.BinanceSmartChain
|
|
237
|
-
? TCBscDepositABI
|
|
238
|
-
: TCEthereumVaultAbi;
|
|
239
|
-
|
|
240
|
-
return (await (
|
|
241
|
-
walletInstance as EVMWallet<typeof AVAXToolbox | typeof ETHToolbox | typeof BSCToolbox>
|
|
242
|
-
).call({
|
|
243
|
-
abi,
|
|
244
|
-
contractAddress:
|
|
245
|
-
router || ((await this.#getInboundDataByChain(chain as EVMChain)).router as string),
|
|
246
|
-
funcName: 'depositWithExpiry',
|
|
247
|
-
funcParams: [
|
|
248
|
-
recipient,
|
|
249
|
-
getChecksumAddressFromAsset({ chain, symbol, ticker }, chain),
|
|
250
|
-
// TODO: (@Towan) Re-Check on that conversion 🙏
|
|
251
|
-
assetValue.baseValueBigInt.toString(),
|
|
252
|
-
params.memo,
|
|
253
|
-
rest.expiration,
|
|
254
|
-
],
|
|
255
|
-
txOverrides: { from: params.from, value: assetValue.baseValueBigInt },
|
|
256
|
-
})) as Promise<string>;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
default: {
|
|
260
|
-
return await walletInstance.transfer(params);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
} catch (error: any) {
|
|
264
|
-
const errorMessage = (error?.message || error?.toString()).toLowerCase();
|
|
265
|
-
const isInsufficientFunds = errorMessage?.includes('insufficient funds');
|
|
266
|
-
const isGas = errorMessage?.includes('gas');
|
|
267
|
-
const isServer = errorMessage?.includes('server');
|
|
268
|
-
const errorKey: Keys = isInsufficientFunds
|
|
269
|
-
? 'core_transaction_deposit_insufficient_funds_error'
|
|
270
|
-
: isGas
|
|
271
|
-
? 'core_transaction_deposit_gas_error'
|
|
272
|
-
: isServer
|
|
273
|
-
? 'core_transaction_deposit_server_error'
|
|
274
|
-
: 'core_transaction_deposit_error';
|
|
275
|
-
|
|
276
|
-
throw new SwapKitError(errorKey, error);
|
|
277
|
-
}
|
|
278
|
-
};
|
|
116
|
+
return acc;
|
|
117
|
+
}, {} as ConnectWalletMethods);
|
|
279
118
|
|
|
280
119
|
/**
|
|
281
|
-
*
|
|
120
|
+
* @Private
|
|
121
|
+
* Internal helpers
|
|
282
122
|
*/
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
runeAssetValue: AssetValue;
|
|
288
|
-
assetValue: AssetValue;
|
|
289
|
-
}) => {
|
|
290
|
-
if (runeAssetValue.lte(0) || assetValue.lte(0)) {
|
|
291
|
-
throw new SwapKitError('core_transaction_create_liquidity_invalid_params');
|
|
292
|
-
}
|
|
123
|
+
function getSwapKitPlugin(pluginName?: PluginName) {
|
|
124
|
+
const plugin =
|
|
125
|
+
(availablePlugins as SwapKitPlugins)[pluginName as PluginName] ||
|
|
126
|
+
Object.values(availablePlugins)[0];
|
|
293
127
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
try {
|
|
298
|
-
runeTx = await this.#depositToPool({
|
|
299
|
-
assetValue: runeAssetValue,
|
|
300
|
-
memo: getMemoFor(MemoType.DEPOSIT, {
|
|
301
|
-
...assetValue,
|
|
302
|
-
address: this.getAddress(assetValue.chain),
|
|
303
|
-
}),
|
|
304
|
-
});
|
|
305
|
-
} catch (error) {
|
|
306
|
-
throw new SwapKitError('core_transaction_create_liquidity_rune_error', error);
|
|
128
|
+
if (!plugin) {
|
|
129
|
+
throw new SwapKitError("core_plugin_not_found", "Could not find the requested plugin");
|
|
307
130
|
}
|
|
308
131
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
assetValue,
|
|
312
|
-
memo: getMemoFor(MemoType.DEPOSIT, {
|
|
313
|
-
...assetValue,
|
|
314
|
-
address: this.getAddress(Chain.THORChain),
|
|
315
|
-
}),
|
|
316
|
-
});
|
|
317
|
-
} catch (error) {
|
|
318
|
-
throw new SwapKitError('core_transaction_create_liquidity_asset_error', error);
|
|
319
|
-
}
|
|
132
|
+
return plugin;
|
|
133
|
+
}
|
|
320
134
|
|
|
321
|
-
|
|
322
|
-
|
|
135
|
+
function addChain(connectWallet: ChainWallet<Chain>) {
|
|
136
|
+
(connectedWallets[connectWallet.chain as Chain] as ChainWallet<Chain>) = connectWallet;
|
|
137
|
+
}
|
|
323
138
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
139
|
+
/**
|
|
140
|
+
* @Private
|
|
141
|
+
* Wallet interaction helpers
|
|
142
|
+
*/
|
|
143
|
+
function approve<T extends ApproveMode>({
|
|
327
144
|
assetValue,
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
isPendingSymmAsset,
|
|
331
|
-
mode = 'sym',
|
|
145
|
+
type = "checkOnly" as T,
|
|
146
|
+
contractAddress,
|
|
332
147
|
}: {
|
|
333
|
-
|
|
334
|
-
runeAssetValue: AssetValue;
|
|
148
|
+
type: T;
|
|
335
149
|
assetValue: AssetValue;
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
const [chain, ...symbolPath] = poolIdentifier.split('.') as [Chain, string];
|
|
342
|
-
const isSym = mode === 'sym';
|
|
343
|
-
const runeTransfer = runeAssetValue?.gt(0) && (isSym || mode === 'rune');
|
|
344
|
-
const assetTransfer = assetValue?.gt(0) && (isSym || mode === 'asset');
|
|
345
|
-
const includeRuneAddress = isPendingSymmAsset || runeTransfer;
|
|
346
|
-
const runeAddress = includeRuneAddress ? runeAddr || this.getAddress(Chain.THORChain) : '';
|
|
347
|
-
const assetAddress = isSym || mode === 'asset' ? assetAddr || this.getAddress(chain) : '';
|
|
348
|
-
|
|
349
|
-
if (!runeTransfer && !assetTransfer) {
|
|
350
|
-
throw new SwapKitError('core_transaction_add_liquidity_invalid_params');
|
|
351
|
-
}
|
|
352
|
-
if (includeRuneAddress && !runeAddress) {
|
|
353
|
-
throw new SwapKitError('core_transaction_add_liquidity_no_rune_address');
|
|
354
|
-
}
|
|
150
|
+
contractAddress: string;
|
|
151
|
+
}): ApproveReturnType<T> {
|
|
152
|
+
const { address, chain, isGasAsset, isSynthetic } = assetValue;
|
|
153
|
+
const isEVMChain = [Chain.Ethereum, Chain.Avalanche, Chain.BinanceSmartChain].includes(chain);
|
|
154
|
+
const isNativeEVM = isEVMChain && isGasAsset;
|
|
355
155
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
if (runeTransfer && runeAssetValue) {
|
|
360
|
-
try {
|
|
361
|
-
runeTx = await this.#depositToPool({
|
|
362
|
-
assetValue: runeAssetValue,
|
|
363
|
-
memo: getMemoFor(MemoType.DEPOSIT, { ...sharedParams, address: assetAddress }),
|
|
364
|
-
});
|
|
365
|
-
} catch (error) {
|
|
366
|
-
throw new SwapKitError('core_transaction_add_liquidity_rune_error', error);
|
|
367
|
-
}
|
|
156
|
+
if (isNativeEVM || !isEVMChain || isSynthetic) {
|
|
157
|
+
return Promise.resolve(type === "checkOnly" ? true : "approved") as ApproveReturnType<T>;
|
|
368
158
|
}
|
|
369
159
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
assetTx = await this.#depositToPool({
|
|
373
|
-
assetValue,
|
|
374
|
-
memo: getMemoFor(MemoType.DEPOSIT, { ...sharedParams, address: runeAddress }),
|
|
375
|
-
});
|
|
376
|
-
} catch (error) {
|
|
377
|
-
throw new SwapKitError('core_transaction_add_liquidity_asset_error', error);
|
|
378
|
-
}
|
|
379
|
-
}
|
|
160
|
+
const walletMethods =
|
|
161
|
+
connectedWallets[chain as Chain.Ethereum | Chain.BinanceSmartChain | Chain.Avalanche];
|
|
380
162
|
|
|
381
|
-
|
|
382
|
-
};
|
|
163
|
+
const walletAction = type === "checkOnly" ? walletMethods?.isApproved : walletMethods?.approve;
|
|
383
164
|
|
|
384
|
-
|
|
385
|
-
memo,
|
|
386
|
-
assetValue,
|
|
387
|
-
percent,
|
|
388
|
-
from,
|
|
389
|
-
to,
|
|
390
|
-
}: {
|
|
391
|
-
memo?: string;
|
|
392
|
-
assetValue: AssetValue;
|
|
393
|
-
percent: number;
|
|
394
|
-
from: 'sym' | 'rune' | 'asset';
|
|
395
|
-
to: 'sym' | 'rune' | 'asset';
|
|
396
|
-
}) => {
|
|
397
|
-
const targetAsset =
|
|
398
|
-
to === 'rune'
|
|
399
|
-
? AssetValue.fromChainOrSignature(Chain.THORChain)
|
|
400
|
-
: (from === 'sym' && to === 'sym') || from === 'rune' || from === 'asset'
|
|
401
|
-
? undefined
|
|
402
|
-
: assetValue;
|
|
403
|
-
|
|
404
|
-
try {
|
|
405
|
-
const txHash = await this.#depositToPool({
|
|
406
|
-
assetValue: getMinAmountByChain(from === 'asset' ? assetValue.chain : Chain.THORChain),
|
|
407
|
-
memo:
|
|
408
|
-
memo ||
|
|
409
|
-
getMemoFor(MemoType.WITHDRAW, {
|
|
410
|
-
symbol: assetValue.symbol,
|
|
411
|
-
chain: assetValue.chain,
|
|
412
|
-
ticker: assetValue.ticker,
|
|
413
|
-
basisPoints: Math.max(10000, Math.round(percent * 100)),
|
|
414
|
-
targetAssetString: targetAsset?.toString(),
|
|
415
|
-
singleSide: false,
|
|
416
|
-
}),
|
|
417
|
-
});
|
|
418
|
-
|
|
419
|
-
return txHash;
|
|
420
|
-
} catch (error) {
|
|
421
|
-
throw new SwapKitError('core_transaction_withdraw_error', error);
|
|
422
|
-
}
|
|
423
|
-
};
|
|
424
|
-
|
|
425
|
-
savings = async ({
|
|
426
|
-
assetValue,
|
|
427
|
-
memo,
|
|
428
|
-
percent,
|
|
429
|
-
type,
|
|
430
|
-
}: { assetValue: AssetValue; memo?: string } & (
|
|
431
|
-
| { type: 'add'; percent?: undefined }
|
|
432
|
-
| { type: 'withdraw'; percent: number }
|
|
433
|
-
)) => {
|
|
434
|
-
const memoType = type === 'add' ? MemoType.DEPOSIT : MemoType.WITHDRAW;
|
|
435
|
-
const memoString =
|
|
436
|
-
memo ||
|
|
437
|
-
getMemoFor(memoType, {
|
|
438
|
-
ticker: assetValue.ticker,
|
|
439
|
-
symbol: assetValue.symbol,
|
|
440
|
-
chain: assetValue.chain,
|
|
441
|
-
singleSide: true,
|
|
442
|
-
basisPoints: percent ? Math.max(10000, Math.round(percent * 100)) : undefined,
|
|
443
|
-
});
|
|
444
|
-
|
|
445
|
-
return this.#depositToPool({ assetValue, memo: memoString });
|
|
446
|
-
};
|
|
447
|
-
|
|
448
|
-
loan = ({
|
|
449
|
-
assetValue,
|
|
450
|
-
memo,
|
|
451
|
-
minAmount,
|
|
452
|
-
type,
|
|
453
|
-
}: {
|
|
454
|
-
assetValue: AssetValue;
|
|
455
|
-
memo?: string;
|
|
456
|
-
minAmount: AssetValue;
|
|
457
|
-
type: 'open' | 'close';
|
|
458
|
-
}) =>
|
|
459
|
-
this.#depositToPool({
|
|
460
|
-
assetValue,
|
|
461
|
-
memo:
|
|
462
|
-
memo ||
|
|
463
|
-
getMemoFor(type === 'open' ? MemoType.OPEN_LOAN : MemoType.CLOSE_LOAN, {
|
|
464
|
-
asset: assetValue.toString(),
|
|
465
|
-
minAmount: minAmount.toString(),
|
|
466
|
-
address: this.getAddress(assetValue.chain),
|
|
467
|
-
}),
|
|
468
|
-
});
|
|
165
|
+
if (!walletAction) throw new SwapKitError("core_wallet_connection_not_found");
|
|
469
166
|
|
|
470
|
-
|
|
471
|
-
type,
|
|
472
|
-
assetValue,
|
|
473
|
-
address,
|
|
474
|
-
}: { address: string } & (
|
|
475
|
-
| { type: 'bond' | 'unbond'; assetValue: AssetValue }
|
|
476
|
-
| { type: 'leave'; assetValue?: undefined }
|
|
477
|
-
)) => {
|
|
478
|
-
const memoType =
|
|
479
|
-
type === 'bond' ? MemoType.BOND : type === 'unbond' ? MemoType.UNBOND : MemoType.LEAVE;
|
|
480
|
-
const memo = getMemoFor(memoType, {
|
|
481
|
-
address,
|
|
482
|
-
unbondAmount: type === 'unbond' ? assetValue.baseValueNumber : undefined,
|
|
483
|
-
});
|
|
167
|
+
const from = getAddress(chain);
|
|
484
168
|
|
|
485
|
-
|
|
486
|
-
memo,
|
|
487
|
-
assetValue: type === 'bond' ? assetValue : getMinAmountByChain(Chain.THORChain),
|
|
488
|
-
});
|
|
489
|
-
};
|
|
169
|
+
if (!(address && from)) throw new SwapKitError("core_approve_asset_address_or_from_not_found");
|
|
490
170
|
|
|
491
|
-
|
|
492
|
-
assetValue,
|
|
493
|
-
...param
|
|
494
|
-
}: ThornameRegisterParam & { assetValue: AssetValue }) =>
|
|
495
|
-
this.#thorchainTransfer({ assetValue, memo: getMemoFor(MemoType.THORNAME_REGISTER, param) });
|
|
496
|
-
|
|
497
|
-
extend = ({ wallets, config, apis = {}, rpcUrls = {} }: ExtendParams<T>) => {
|
|
498
|
-
try {
|
|
499
|
-
wallets.forEach((wallet) => {
|
|
500
|
-
// @ts-expect-error ANCHOR - Not Worth
|
|
501
|
-
this[wallet.connectMethodName] = wallet.connect({
|
|
502
|
-
addChain: this.#addConnectedChain,
|
|
503
|
-
config: config || {},
|
|
504
|
-
apis,
|
|
505
|
-
rpcUrls,
|
|
506
|
-
});
|
|
507
|
-
});
|
|
508
|
-
} catch (error) {
|
|
509
|
-
throw new SwapKitError('core_extend_error', error);
|
|
510
|
-
}
|
|
511
|
-
};
|
|
171
|
+
const spenderAddress = contractAddress;
|
|
512
172
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
}
|
|
520
|
-
const walletMethods = this.getWallet<typeof chain>(chain);
|
|
521
|
-
|
|
522
|
-
switch (chain) {
|
|
523
|
-
case Chain.Arbitrum:
|
|
524
|
-
case Chain.Avalanche:
|
|
525
|
-
case Chain.BinanceSmartChain:
|
|
526
|
-
case Chain.Ethereum:
|
|
527
|
-
case Chain.Optimism:
|
|
528
|
-
case Chain.Polygon: {
|
|
529
|
-
const { estimateMaxSendableAmount } = await import('@swapkit/toolbox-evm');
|
|
530
|
-
return estimateMaxSendableAmount({
|
|
531
|
-
...params,
|
|
532
|
-
toolbox: walletMethods as EVMToolbox,
|
|
533
|
-
});
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
case Chain.Bitcoin:
|
|
537
|
-
case Chain.BitcoinCash:
|
|
538
|
-
case Chain.Dogecoin:
|
|
539
|
-
case Chain.Litecoin:
|
|
540
|
-
return (walletMethods as UTXOToolbox).estimateMaxSendableAmount(params);
|
|
541
|
-
|
|
542
|
-
case Chain.Binance:
|
|
543
|
-
case Chain.THORChain:
|
|
544
|
-
case Chain.Cosmos: {
|
|
545
|
-
const { estimateMaxSendableAmount } = await import('@swapkit/toolbox-cosmos');
|
|
546
|
-
return estimateMaxSendableAmount({
|
|
547
|
-
...params,
|
|
548
|
-
toolbox: walletMethods as CosmosLikeToolbox,
|
|
549
|
-
});
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
default:
|
|
553
|
-
throw new SwapKitError('core_estimated_max_spendable_chain_not_supported');
|
|
554
|
-
}
|
|
555
|
-
};
|
|
173
|
+
return walletAction({
|
|
174
|
+
amount: assetValue.getBaseValue("bigint"),
|
|
175
|
+
assetAddress: address,
|
|
176
|
+
from,
|
|
177
|
+
spenderAddress,
|
|
178
|
+
}) as ApproveReturnType<T>;
|
|
179
|
+
}
|
|
556
180
|
|
|
557
181
|
/**
|
|
558
|
-
*
|
|
182
|
+
* @Public
|
|
183
|
+
* Wallet helpers
|
|
559
184
|
*/
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
}
|
|
575
|
-
connectTrezor = async (_chains: Chain, _derivationPath: number[]): Promise<void> => {
|
|
576
|
-
throw new SwapKitError('core_wallet_trezor_not_installed');
|
|
577
|
-
};
|
|
578
|
-
connectKeplr = async (_chain: Chain): Promise<void> => {
|
|
579
|
-
throw new SwapKitError('core_wallet_keplr_not_installed');
|
|
580
|
-
};
|
|
581
|
-
connectOkx = async (_chains: Chain[]): Promise<void> => {
|
|
582
|
-
throw new SwapKitError('core_wallet_okx_not_installed');
|
|
583
|
-
};
|
|
584
|
-
disconnectChain = (chain: Chain) => {
|
|
585
|
-
this.connectedChains[chain] = null;
|
|
586
|
-
this.connectedWallets[chain] = null;
|
|
587
|
-
};
|
|
588
|
-
|
|
589
|
-
#getInboundDataByChain = async (chain: Chain) => {
|
|
590
|
-
if (chain === Chain.THORChain) {
|
|
591
|
-
return {
|
|
592
|
-
gas_rate: '0',
|
|
593
|
-
router: '0',
|
|
594
|
-
address: '',
|
|
595
|
-
halted: false,
|
|
596
|
-
chain: Chain.THORChain,
|
|
597
|
-
};
|
|
598
|
-
}
|
|
599
|
-
const inboundData = await getInboundData(this.stagenet);
|
|
600
|
-
const chainAddressData = inboundData.find((item) => item.chain === chain);
|
|
601
|
-
|
|
602
|
-
if (!chainAddressData) throw new SwapKitError('core_inbound_data_not_found');
|
|
603
|
-
if (chainAddressData?.halted) throw new SwapKitError('core_chain_halted');
|
|
604
|
-
|
|
605
|
-
return chainAddressData;
|
|
606
|
-
};
|
|
607
|
-
|
|
608
|
-
#addConnectedChain = ({ chain, wallet, walletMethods }: AddChainWalletParams) => {
|
|
609
|
-
this.connectedChains[chain] = wallet;
|
|
610
|
-
this.connectedWallets[chain] = walletMethods;
|
|
611
|
-
};
|
|
612
|
-
|
|
613
|
-
#approve = async <T = string>({
|
|
614
|
-
assetValue: { baseValueBigInt, address, chain, isGasAsset, isSynthetic },
|
|
615
|
-
type = 'checkOnly',
|
|
616
|
-
contractAddress,
|
|
617
|
-
}: {
|
|
618
|
-
assetValue: AssetValue;
|
|
619
|
-
type?: 'checkOnly' | 'approve';
|
|
620
|
-
contractAddress?: string;
|
|
621
|
-
}) => {
|
|
622
|
-
const isEVMChain = [Chain.Ethereum, Chain.Avalanche, Chain.BinanceSmartChain].includes(chain);
|
|
623
|
-
const isNativeEVM = isEVMChain && isGasAsset;
|
|
185
|
+
function getWallet(chain: Chain) {
|
|
186
|
+
return connectedWallets[chain];
|
|
187
|
+
}
|
|
188
|
+
function getAddress(chain: Chain) {
|
|
189
|
+
return getWallet(chain)?.address || "";
|
|
190
|
+
}
|
|
191
|
+
function getBalance(chain: Chain) {
|
|
192
|
+
return getWallet(chain)?.balance || [];
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* TODO: Figure out validation without connecting to wallet
|
|
196
|
+
*/
|
|
197
|
+
function validateAddress({ address, chain }: { address: string; chain: Chain }) {
|
|
198
|
+
return getWallet(chain)?.validateAddress?.(address);
|
|
199
|
+
}
|
|
624
200
|
|
|
625
|
-
|
|
201
|
+
async function getWalletWithBalance(chain: Chain, potentialScamFilter?: boolean) {
|
|
202
|
+
const defaultBalance = [AssetValue.fromChainOrSignature(chain)];
|
|
203
|
+
const wallet = getWallet(chain);
|
|
626
204
|
|
|
627
|
-
|
|
628
|
-
|
|
205
|
+
if (!wallet) {
|
|
206
|
+
throw new SwapKitError("core_wallet_connection_not_found");
|
|
207
|
+
}
|
|
629
208
|
|
|
630
|
-
|
|
209
|
+
const balance = await wallet?.getBalance(wallet.address, potentialScamFilter);
|
|
210
|
+
wallet.balance = balance?.length ? balance : defaultBalance;
|
|
631
211
|
|
|
632
|
-
|
|
212
|
+
return wallet;
|
|
213
|
+
}
|
|
633
214
|
|
|
634
|
-
|
|
215
|
+
/**
|
|
216
|
+
* @Public
|
|
217
|
+
* Wallet interaction methods
|
|
218
|
+
*/
|
|
219
|
+
function approveAssetValue(assetValue: AssetValue, contractAddress: string) {
|
|
220
|
+
return approve({ assetValue, contractAddress, type: ApproveMode.Approve });
|
|
221
|
+
}
|
|
635
222
|
|
|
636
|
-
|
|
637
|
-
|
|
223
|
+
function isAssetValueApproved(assetValue: AssetValue, contractAddress: string) {
|
|
224
|
+
return approve({ assetValue, contractAddress, type: ApproveMode.CheckOnly });
|
|
225
|
+
}
|
|
638
226
|
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
assetAddress: address,
|
|
642
|
-
from,
|
|
643
|
-
spenderAddress,
|
|
644
|
-
}) as Promise<T>;
|
|
645
|
-
};
|
|
227
|
+
function swap({ provider, ...rest }: SwapParams) {
|
|
228
|
+
const plugin = getSwapKitPlugin(provider?.name);
|
|
646
229
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
memo,
|
|
650
|
-
feeOptionKey = FeeOption.Fast,
|
|
651
|
-
}: {
|
|
652
|
-
assetValue: AssetValue;
|
|
653
|
-
memo: string;
|
|
654
|
-
feeOptionKey?: FeeOption;
|
|
655
|
-
}) => {
|
|
656
|
-
const {
|
|
657
|
-
gas_rate,
|
|
658
|
-
router,
|
|
659
|
-
address: poolAddress,
|
|
660
|
-
} = await this.#getInboundDataByChain(assetValue.chain);
|
|
661
|
-
const feeRate = (parseInt(gas_rate) || 0) * gasFeeMultiplier[feeOptionKey];
|
|
662
|
-
|
|
663
|
-
return this.deposit({
|
|
664
|
-
assetValue,
|
|
665
|
-
recipient: poolAddress,
|
|
666
|
-
memo,
|
|
667
|
-
router,
|
|
668
|
-
feeRate,
|
|
669
|
-
});
|
|
670
|
-
};
|
|
230
|
+
return plugin.swap({ provider, ...rest });
|
|
231
|
+
}
|
|
671
232
|
|
|
672
|
-
|
|
673
|
-
|
|
233
|
+
return {
|
|
234
|
+
...availablePlugins,
|
|
235
|
+
...connectWalletMethods,
|
|
674
236
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
throw new SwapKitError('core_chain_halted');
|
|
678
|
-
}
|
|
237
|
+
getExplorerAddressUrl: getAddressUrl,
|
|
238
|
+
getExplorerTxUrl: getTxUrl,
|
|
679
239
|
|
|
680
|
-
|
|
240
|
+
approveAssetValue,
|
|
241
|
+
getAddress,
|
|
242
|
+
getBalance,
|
|
243
|
+
getWallet,
|
|
244
|
+
getWalletWithBalance,
|
|
245
|
+
isAssetValueApproved,
|
|
246
|
+
swap,
|
|
247
|
+
validateAddress,
|
|
681
248
|
};
|
|
682
|
-
|
|
683
|
-
#prepareTxParams = ({ assetValue, ...restTxParams }: CoreTxParams & { router?: string }) => ({
|
|
684
|
-
...restTxParams,
|
|
685
|
-
memo: restTxParams.memo || '',
|
|
686
|
-
from: this.getAddress(assetValue.chain),
|
|
687
|
-
assetValue,
|
|
688
|
-
});
|
|
689
249
|
}
|