@exodus/solana-api 2.5.18 → 2.5.20
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/lib/account-state.js +48 -0
- package/lib/api.js +1161 -0
- package/lib/connection.js +262 -0
- package/lib/index.js +68 -0
- package/lib/pay/fetchTransaction.js +157 -0
- package/lib/pay/index.js +57 -0
- package/lib/pay/parseURL.js +120 -0
- package/lib/pay/prepareSendData.js +38 -0
- package/lib/pay/validateBeforePay.js +44 -0
- package/lib/tx-log/index.js +18 -0
- package/lib/tx-log/solana-monitor.js +354 -0
- package/package.json +3 -3
- package/src/__tests__/api.test.js +286 -0
- package/src/__tests__/assets.js +7 -0
- package/src/__tests__/fixtures.js +3166 -0
- package/src/__tests__/index.test.js +7 -0
- package/src/__tests__/staking.test.js +85 -0
- package/src/__tests__/token.test.js +374 -0
- package/src/__tests__/ws.test.js +74 -0
- package/src/api.js +1 -1
- package/src/connection.js +21 -16
- package/src/tx-log/__tests__/solana-monitor-api-mock.js +353 -0
- package/src/tx-log/__tests__/solana-monitor.integration.test.js +119 -0
- package/src/tx-log/__tests__/solana-monitor.test.js +132 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.ValidateError = void 0;
|
|
7
|
+
exports.validateBeforePay = validateBeforePay;
|
|
8
|
+
|
|
9
|
+
var _ = _interopRequireDefault(require(".."));
|
|
10
|
+
|
|
11
|
+
var _currency = _interopRequireDefault(require("@exodus/currency"));
|
|
12
|
+
|
|
13
|
+
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
14
|
+
|
|
15
|
+
class ValidateError extends Error {
|
|
16
|
+
constructor(...args) {
|
|
17
|
+
super(...args);
|
|
18
|
+
this.name = 'ValidateError';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
exports.ValidateError = ValidateError;
|
|
24
|
+
|
|
25
|
+
async function validateBeforePay({
|
|
26
|
+
asset,
|
|
27
|
+
senderInfo,
|
|
28
|
+
amount,
|
|
29
|
+
feeAmount = 0,
|
|
30
|
+
recipient,
|
|
31
|
+
checkEnoughBalance = true
|
|
32
|
+
}) {
|
|
33
|
+
const isNative = asset.name === asset.baseAsset.name;
|
|
34
|
+
const recipientInfo = await _.default.getAccountInfo(recipient);
|
|
35
|
+
if (!recipientInfo) throw new ValidateError(`recipient ${recipient} not found`);
|
|
36
|
+
|
|
37
|
+
if (checkEnoughBalance) {
|
|
38
|
+
const totalAmountMustPay = asset.currency.defaultUnit(amount).add(feeAmount);
|
|
39
|
+
const currentBalance = isNative ? senderInfo.balance : senderInfo.tokenBalances[asset.name];
|
|
40
|
+
if (totalAmountMustPay.gt(currentBalance)) throw new ValidateError(`insufficient funds`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
|
|
7
|
+
var _solanaMonitor = require("./solana-monitor");
|
|
8
|
+
|
|
9
|
+
Object.keys(_solanaMonitor).forEach(function (key) {
|
|
10
|
+
if (key === "default" || key === "__esModule") return;
|
|
11
|
+
if (key in exports && exports[key] === _solanaMonitor[key]) return;
|
|
12
|
+
Object.defineProperty(exports, key, {
|
|
13
|
+
enumerable: true,
|
|
14
|
+
get: function () {
|
|
15
|
+
return _solanaMonitor[key];
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
});
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.SolanaMonitor = void 0;
|
|
7
|
+
|
|
8
|
+
var _assetLib = require("@exodus/asset-lib");
|
|
9
|
+
|
|
10
|
+
var _lodash = _interopRequireDefault(require("lodash"));
|
|
11
|
+
|
|
12
|
+
var _minimalisticAssert = _interopRequireDefault(require("minimalistic-assert"));
|
|
13
|
+
|
|
14
|
+
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
15
|
+
|
|
16
|
+
const DEFAULT_POOL_ADDRESS = '9QU2QSxhb24FUX3Tu2FpczXjpK3VYrvRudywSZaM29mF'; // Everstake
|
|
17
|
+
|
|
18
|
+
const DEFAULT_REMOTE_CONFIG = {
|
|
19
|
+
rpcs: [],
|
|
20
|
+
ws: [],
|
|
21
|
+
staking: {
|
|
22
|
+
enabled: true,
|
|
23
|
+
pool: DEFAULT_POOL_ADDRESS
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
class SolanaMonitor extends _assetLib.BaseMonitor {
|
|
28
|
+
constructor({
|
|
29
|
+
api,
|
|
30
|
+
includeUnparsed = false,
|
|
31
|
+
...args
|
|
32
|
+
}) {
|
|
33
|
+
super(args);
|
|
34
|
+
(0, _minimalisticAssert.default)(api, 'api is required');
|
|
35
|
+
this.api = api;
|
|
36
|
+
this.cursors = {};
|
|
37
|
+
this.assets = {};
|
|
38
|
+
this.includeUnparsed = includeUnparsed;
|
|
39
|
+
this.addHook('before-stop', (...args) => this.beforeStop(...args));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async beforeStop() {
|
|
43
|
+
const walletAccounts = await this.aci.getWalletAccounts({
|
|
44
|
+
assetName: this.asset.name
|
|
45
|
+
});
|
|
46
|
+
return Promise.all(walletAccounts.map(walletAccount => this.stopListener({
|
|
47
|
+
walletAccount
|
|
48
|
+
})));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async initWalletAccount({
|
|
52
|
+
walletAccount
|
|
53
|
+
}) {
|
|
54
|
+
if (this.tickCount[walletAccount] === 0) {
|
|
55
|
+
await this.startListener({
|
|
56
|
+
walletAccount
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async startListener({
|
|
62
|
+
walletAccount
|
|
63
|
+
}) {
|
|
64
|
+
const address = await this.aci.getReceiveAddress({
|
|
65
|
+
assetName: this.asset.name,
|
|
66
|
+
walletAccount
|
|
67
|
+
});
|
|
68
|
+
return this.api.watchAddress({
|
|
69
|
+
address
|
|
70
|
+
/*
|
|
71
|
+
// OPTIONAL. Relying on polling through ws
|
|
72
|
+
tokensAddresses: [], // needed for ASA subs
|
|
73
|
+
handleAccounts: (updates) => this.accountsCallback({ updates, walletAccount }),
|
|
74
|
+
handleTransfers: (txs) => {
|
|
75
|
+
// new SOL tx, ticking monitor
|
|
76
|
+
this.tick({ walletAccount }) // it will cause refresh for both sender/receiver. Without necessarily fetching the tx if it's not finalized in the node.
|
|
77
|
+
},
|
|
78
|
+
*/
|
|
79
|
+
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async stopListener({
|
|
84
|
+
walletAccount
|
|
85
|
+
}) {
|
|
86
|
+
const address = await this.aci.getReceiveAddress({
|
|
87
|
+
assetName: this.asset.name,
|
|
88
|
+
walletAccount
|
|
89
|
+
});
|
|
90
|
+
return this.api.unwatchAddress({
|
|
91
|
+
address
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
setServer(config = {}) {
|
|
96
|
+
const {
|
|
97
|
+
rpcs,
|
|
98
|
+
ws,
|
|
99
|
+
staking = {}
|
|
100
|
+
} = { ...DEFAULT_REMOTE_CONFIG,
|
|
101
|
+
...config
|
|
102
|
+
};
|
|
103
|
+
this.api.setServer(rpcs[0]);
|
|
104
|
+
this.api.setWsEndpoint(ws[0]);
|
|
105
|
+
this.staking = staking;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
hasNewCursor({
|
|
109
|
+
walletAccount,
|
|
110
|
+
cursorState
|
|
111
|
+
}) {
|
|
112
|
+
const {
|
|
113
|
+
cursor
|
|
114
|
+
} = cursorState;
|
|
115
|
+
return this.cursors[walletAccount] !== cursor;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async emitUnknownTokensEvent({
|
|
119
|
+
tokenAccounts
|
|
120
|
+
}) {
|
|
121
|
+
const tokensList = await this.api.getWalletTokensList({
|
|
122
|
+
tokenAccounts
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
if (tokensList.length > 0) {
|
|
126
|
+
this.emit('unknown-tokens', tokensList);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async tick({
|
|
131
|
+
walletAccount,
|
|
132
|
+
refresh
|
|
133
|
+
}) {
|
|
134
|
+
// Check for new wallet account
|
|
135
|
+
await this.initWalletAccount({
|
|
136
|
+
walletAccount
|
|
137
|
+
});
|
|
138
|
+
const assetName = this.asset.name;
|
|
139
|
+
this.assets = await this.aci.getAssetsForNetwork({
|
|
140
|
+
baseAssetName: assetName
|
|
141
|
+
});
|
|
142
|
+
this.api.setTokens(this.assets);
|
|
143
|
+
const accountState = await this.aci.getAccountState({
|
|
144
|
+
assetName,
|
|
145
|
+
walletAccount
|
|
146
|
+
});
|
|
147
|
+
const address = await this.aci.getReceiveAddress({
|
|
148
|
+
assetName,
|
|
149
|
+
walletAccount
|
|
150
|
+
});
|
|
151
|
+
const {
|
|
152
|
+
logItemsByAsset,
|
|
153
|
+
hasNewTxs,
|
|
154
|
+
cursorState
|
|
155
|
+
} = await this.getHistory({
|
|
156
|
+
address,
|
|
157
|
+
accountState,
|
|
158
|
+
walletAccount,
|
|
159
|
+
refresh
|
|
160
|
+
});
|
|
161
|
+
let staking = accountState.mem;
|
|
162
|
+
const cursorChanged = this.hasNewCursor({
|
|
163
|
+
walletAccount,
|
|
164
|
+
cursorState
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
if (refresh || cursorChanged) {
|
|
168
|
+
staking = await this.updateStakingInfo({
|
|
169
|
+
walletAccount,
|
|
170
|
+
address
|
|
171
|
+
});
|
|
172
|
+
this.cursors[walletAccount] = cursorState.cursor;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
await this.updateTxLogByAsset({
|
|
176
|
+
walletAccount,
|
|
177
|
+
logItemsByAsset,
|
|
178
|
+
refresh
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
if (refresh || hasNewTxs || cursorChanged) {
|
|
182
|
+
const tokenAccounts = await this.api.getTokenAccountsByOwner(address);
|
|
183
|
+
await this.emitUnknownTokensEvent({
|
|
184
|
+
tokenAccounts
|
|
185
|
+
});
|
|
186
|
+
const account = await this.getAccount({
|
|
187
|
+
address,
|
|
188
|
+
staking,
|
|
189
|
+
tokenAccounts
|
|
190
|
+
});
|
|
191
|
+
await this.updateState({
|
|
192
|
+
account,
|
|
193
|
+
cursorState,
|
|
194
|
+
walletAccount
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async getHistory({
|
|
200
|
+
address,
|
|
201
|
+
accountState,
|
|
202
|
+
refresh
|
|
203
|
+
} = {}) {
|
|
204
|
+
let cursor = refresh ? '' : accountState.cursor;
|
|
205
|
+
const baseAsset = this.asset;
|
|
206
|
+
const {
|
|
207
|
+
transactions,
|
|
208
|
+
newCursor
|
|
209
|
+
} = await this.api.getTransactions(address, {
|
|
210
|
+
cursor,
|
|
211
|
+
includeUnparsed: this.includeUnparsed
|
|
212
|
+
});
|
|
213
|
+
const mappedTransactions = [];
|
|
214
|
+
|
|
215
|
+
for (const tx of transactions) {
|
|
216
|
+
const assetName = _lodash.default.get(tx, 'token.tokenName', baseAsset.name);
|
|
217
|
+
|
|
218
|
+
const asset = this.assets[assetName];
|
|
219
|
+
if (assetName === 'unknown' || !asset) continue; // skip unknown tokens
|
|
220
|
+
|
|
221
|
+
const coinAmount = asset.currency.baseUnit(tx.amount).toDefault();
|
|
222
|
+
const item = {
|
|
223
|
+
coinName: assetName,
|
|
224
|
+
txId: tx.id,
|
|
225
|
+
from: [tx.from],
|
|
226
|
+
coinAmount,
|
|
227
|
+
confirmations: 1,
|
|
228
|
+
// tx.confirmations, // avoid multiple notifications
|
|
229
|
+
date: tx.date,
|
|
230
|
+
error: tx.error,
|
|
231
|
+
data: {
|
|
232
|
+
staking: tx.staking || null,
|
|
233
|
+
unparsed: !!tx.unparsed,
|
|
234
|
+
swapTx: !!(tx.data && tx.data.inner)
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
if (tx.owner === address) {
|
|
239
|
+
// send transaction
|
|
240
|
+
item.to = tx.to;
|
|
241
|
+
item.feeAmount = baseAsset.currency.baseUnit(tx.fee).toDefault(); // in SOL
|
|
242
|
+
|
|
243
|
+
item.coinAmount = item.coinAmount.negate();
|
|
244
|
+
|
|
245
|
+
if (tx.to === tx.owner) {
|
|
246
|
+
item.selfSend = true;
|
|
247
|
+
item.coinAmount = asset.currency.ZERO;
|
|
248
|
+
}
|
|
249
|
+
} else if (tx.unparsed) {
|
|
250
|
+
if (tx.fee !== 0) item.feeAmount = baseAsset.currency.baseUnit(tx.fee).toDefault(); // in SOL
|
|
251
|
+
|
|
252
|
+
item.data.meta = tx.data.meta;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (asset.assetType === 'SOLANA_TOKEN' && item.feeAmount && item.feeAmount.isPositive) {
|
|
256
|
+
const feeAsset = asset.feeAsset;
|
|
257
|
+
const feeItem = { ..._lodash.default.clone(item),
|
|
258
|
+
coinName: feeAsset.name,
|
|
259
|
+
tokens: [asset.name],
|
|
260
|
+
coinAmount: feeAsset.currency.ZERO
|
|
261
|
+
};
|
|
262
|
+
mappedTransactions.push(feeItem);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
mappedTransactions.push(item);
|
|
266
|
+
} // logItemsByAsset = { 'solana:': [...], 'serum': [...] }
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
logItemsByAsset: _lodash.default.groupBy(mappedTransactions, item => item.coinName),
|
|
271
|
+
hasNewTxs: transactions.length > 0,
|
|
272
|
+
cursorState: {
|
|
273
|
+
cursor: newCursor
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async getAccount({
|
|
279
|
+
address,
|
|
280
|
+
staking,
|
|
281
|
+
tokenAccounts
|
|
282
|
+
}) {
|
|
283
|
+
const tokens = Object.keys(this.assets).filter(name => name !== this.asset.name);
|
|
284
|
+
const [solBalance, splBalances] = await Promise.all([this.api.getBalance(address), this.api.getTokensBalance({
|
|
285
|
+
address,
|
|
286
|
+
filterByTokens: tokens,
|
|
287
|
+
tokenAccounts
|
|
288
|
+
})]);
|
|
289
|
+
const stakedBalance = this.asset.currency.baseUnit(staking.locked).toDefault();
|
|
290
|
+
const withdrawableBalance = this.asset.currency.baseUnit(staking.withdrawable).toDefault();
|
|
291
|
+
const pendingBalance = this.asset.currency.baseUnit(staking.pending).toDefault();
|
|
292
|
+
const balance = this.asset.currency.baseUnit(solBalance).toDefault().add(stakedBalance).add(withdrawableBalance).add(pendingBalance);
|
|
293
|
+
|
|
294
|
+
const tokenBalances = _lodash.default.mapValues(splBalances, (balance, name) => this.assets[name].currency.baseUnit(balance).toDefault());
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
balance,
|
|
298
|
+
tokenBalances
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async updateState({
|
|
303
|
+
account,
|
|
304
|
+
cursorState,
|
|
305
|
+
walletAccount
|
|
306
|
+
}) {
|
|
307
|
+
const {
|
|
308
|
+
balance,
|
|
309
|
+
tokenBalances
|
|
310
|
+
} = account;
|
|
311
|
+
const newData = {
|
|
312
|
+
balance,
|
|
313
|
+
tokenBalances,
|
|
314
|
+
...cursorState
|
|
315
|
+
};
|
|
316
|
+
return this.updateAccountState({
|
|
317
|
+
newData,
|
|
318
|
+
walletAccount
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async updateStakingInfo({
|
|
323
|
+
walletAccount,
|
|
324
|
+
address
|
|
325
|
+
}) {
|
|
326
|
+
const stakingInfo = await this.api.getStakeAccountsInfo(address);
|
|
327
|
+
const rewards = await this.api.getRewards(Object.keys(stakingInfo.accounts));
|
|
328
|
+
const mem = {
|
|
329
|
+
loaded: true,
|
|
330
|
+
staking: this.staking,
|
|
331
|
+
isDelegating: Object.values(stakingInfo.accounts).some(({
|
|
332
|
+
state
|
|
333
|
+
}) => ['active', 'activating', 'inactive'].includes(state)),
|
|
334
|
+
// true if at least 1 account is delegating
|
|
335
|
+
locked: this.asset.currency.baseUnit(stakingInfo.locked).toDefault(),
|
|
336
|
+
withdrawable: this.asset.currency.baseUnit(stakingInfo.withdrawable).toDefault(),
|
|
337
|
+
pending: this.asset.currency.baseUnit(stakingInfo.pending).toDefault(),
|
|
338
|
+
// still undelegating (not yet available for withdraw)
|
|
339
|
+
earned: this.asset.currency.baseUnit(rewards).toDefault(),
|
|
340
|
+
accounts: stakingInfo.accounts // Obj
|
|
341
|
+
|
|
342
|
+
};
|
|
343
|
+
await this.updateAccountState({
|
|
344
|
+
walletAccount,
|
|
345
|
+
newData: {
|
|
346
|
+
mem
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
return mem;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
exports.SolanaMonitor = SolanaMonitor;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/solana-api",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.20",
|
|
4
4
|
"description": "Exodus internal Solana asset API wrapper",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"files": [
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"@exodus/models": "^10.1.0",
|
|
24
24
|
"@exodus/nfts-core": "^0.5.0",
|
|
25
25
|
"@exodus/simple-retry": "^0.0.6",
|
|
26
|
-
"@exodus/solana-lib": "^1.6.
|
|
26
|
+
"@exodus/solana-lib": "^1.6.11",
|
|
27
27
|
"@exodus/solana-meta": "^1.0.3",
|
|
28
28
|
"bn.js": "^4.11.0",
|
|
29
29
|
"debug": "^4.1.1",
|
|
@@ -34,5 +34,5 @@
|
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@exodus/assets-testing": "file:../../../__testing__"
|
|
36
36
|
},
|
|
37
|
-
"gitHead": "
|
|
37
|
+
"gitHead": "9ec7084bcdfe14f1c6f11df393351885773046a6"
|
|
38
38
|
}
|