/
exchange.ts
291 lines (269 loc) · 6.57 KB
/
exchange.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
import type { TokenInfo } from "@saberhq/token-utils";
import {
deserializeAccount,
deserializeMint,
parseBigintIsh,
Token,
TokenAmount,
} from "@saberhq/token-utils";
import type { Connection, PublicKey } from "@solana/web3.js";
import BN from "bn.js";
import type { default as JSBI } from "jsbi";
import { default as invariant } from "tiny-invariant";
import { SWAP_PROGRAM_ID } from "../constants.js";
import { StableSwap } from "../stable-swap.js";
import type { Fees } from "../state/fees.js";
import type { StableSwapState } from "../state/index.js";
import { loadProgramAccount } from "../util/account.js";
/**
* Reserve information.
*/
export interface IReserve {
/**
* Swap account holding the reserve tokens
*/
reserveAccount: PublicKey;
/**
* Destination account of admin fees of this reserve token
*/
adminFeeAccount: PublicKey;
/**
* Amount of tokens in the reserve
*/
amount: TokenAmount;
}
/**
* Static definition of an exchange.
*/
export interface IExchange {
programID: PublicKey;
swapAccount: PublicKey;
lpToken: Token;
tokens: readonly [Token, Token];
}
/**
* Info loaded from the exchange. This is used by the calculator.
*/
export interface IExchangeInfo {
ampFactor: JSBI;
fees: Fees;
lpTotalSupply: TokenAmount;
reserves: readonly [IReserve, IReserve];
}
/**
* Calculates the amp factor of a swap.
* @param state
* @param now
* @returns
*/
export const calculateAmpFactor = (
state: Pick<
StableSwapState,
| "initialAmpFactor"
| "targetAmpFactor"
| "startRampTimestamp"
| "stopRampTimestamp"
>,
now = Date.now() / 1_000
): JSBI => {
const {
initialAmpFactor,
targetAmpFactor,
startRampTimestamp,
stopRampTimestamp,
} = state;
// The most common case is that there is no ramp in progress.
if (now >= stopRampTimestamp) {
return parseBigintIsh(targetAmpFactor);
}
// If the ramp is about to start, use the initial amp.
if (now <= startRampTimestamp) {
return parseBigintIsh(initialAmpFactor);
}
invariant(
stopRampTimestamp >= startRampTimestamp,
"stop must be after start"
);
// Calculate how far we are along the ramp curve.
const percent =
now >= stopRampTimestamp
? 1
: now <= startRampTimestamp
? 0
: (now - startRampTimestamp) / (stopRampTimestamp - startRampTimestamp);
const diff = Math.floor(
parseFloat(targetAmpFactor.sub(initialAmpFactor).toString()) * percent
);
return parseBigintIsh(initialAmpFactor.add(new BN(diff)));
};
/**
* Creates an IExchangeInfo from parameters.
* @returns
*/
export const makeExchangeInfo = ({
exchange,
swap,
accounts,
}: {
exchange: IExchange;
swap: StableSwap;
accounts: {
reserveA: Buffer;
reserveB: Buffer;
poolMint?: Buffer;
};
}): IExchangeInfo => {
const swapAmountA = deserializeAccount(accounts.reserveA).amount;
const swapAmountB = deserializeAccount(accounts.reserveB).amount;
const poolMintSupply = accounts.poolMint
? deserializeMint(accounts.poolMint).supply
: undefined;
const ampFactor = calculateAmpFactor(swap.state);
return {
ampFactor,
fees: swap.state.fees,
lpTotalSupply: new TokenAmount(exchange.lpToken, poolMintSupply ?? 0),
reserves: [
{
reserveAccount: swap.state.tokenA.reserve,
adminFeeAccount: swap.state.tokenA.adminFeeAccount,
amount: new TokenAmount(exchange.tokens[0], swapAmountA),
},
{
reserveAccount: swap.state.tokenB.reserve,
adminFeeAccount: swap.state.tokenB.adminFeeAccount,
amount: new TokenAmount(exchange.tokens[1], swapAmountB),
},
],
};
};
/**
* Loads exchange info.
* @param exchange
* @param swap
* @returns
*/
export const loadExchangeInfo = async (
connection: Connection,
exchange: IExchange,
swap: StableSwap
): Promise<IExchangeInfo> => {
if (!exchange.programID.equals(swap.config.swapProgramID)) {
throw new Error("Swap program id mismatch");
}
const reserveA = await loadProgramAccount(
connection,
swap.state.tokenA.reserve,
swap.config.tokenProgramID
);
const reserveB = await loadProgramAccount(
connection,
swap.state.tokenB.reserve,
swap.config.tokenProgramID
);
const poolMint = await loadProgramAccount(
connection,
swap.state.poolTokenMint,
swap.config.tokenProgramID
);
return makeExchangeInfo({
swap,
exchange,
accounts: {
reserveA,
reserveB,
poolMint,
},
});
};
/**
* Simplified representation of an IExchange. Useful for configuration.
*/
export interface ExchangeBasic {
/**
* Swap account.
*/
swapAccount: PublicKey;
/**
* Mint of the LP token.
*/
lpToken: PublicKey;
/**
* Info of token A.
*/
tokenA: TokenInfo;
/**
* Info of token B.
*/
tokenB: TokenInfo;
}
/**
* Creates an IExchange from an ExchangeBasic.
* @param tokenMap
* @param param1
* @returns
*/
export const makeExchange = ({
swapAccount,
lpToken,
tokenA,
tokenB,
}: ExchangeBasic): IExchange | null => {
const exchange: IExchange = {
swapAccount,
programID: SWAP_PROGRAM_ID,
lpToken: new Token({
symbol: "SLP",
name: `${tokenA.symbol}-${tokenB.symbol} Saber LP`,
logoURI: "https://app.saber.so/tokens/slp.png",
decimals: tokenA.decimals,
address: lpToken.toString(),
chainId: tokenA.chainId,
tags: ["saber-stableswap-lp"],
}),
tokens: [new Token(tokenA), new Token(tokenB)],
};
return exchange;
};
/**
* Get exchange info from just the swap account.
* @param connection
* @param swapAccount
* @param tokenA
* @param tokenB
* @returns
*/
export const loadExchangeInfoFromSwapAccount = async (
connection: Connection,
swapAccount: PublicKey,
tokenA: Token | undefined = undefined,
tokenB: Token | undefined = undefined
): Promise<IExchangeInfo | null> => {
const stableSwap = await StableSwap.load(connection, swapAccount);
const theTokenA =
tokenA ??
(await Token.load(connection, stableSwap.state.tokenA.mint))?.info;
if (!theTokenA) {
throw new Error(
`Token ${stableSwap.state.tokenA.mint.toString()} not found`
);
}
const theTokenB =
tokenB ??
(await Token.load(connection, stableSwap.state.tokenB.mint))?.info;
if (!theTokenB) {
throw new Error(
`Token ${stableSwap.state.tokenB.mint.toString()} not found`
);
}
const exchange = makeExchange({
swapAccount,
lpToken: stableSwap.state.poolTokenMint,
tokenA: theTokenA,
tokenB: theTokenB,
});
if (exchange === null) {
return null;
}
return await loadExchangeInfo(connection, exchange, stableSwap);
};