@exodus/solana-api 2.5.20 → 2.5.21
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/package.json +3 -3
- package/lib/account-state.js +0 -48
- package/lib/api.js +0 -1161
- package/lib/connection.js +0 -262
- package/lib/index.js +0 -68
- package/lib/pay/fetchTransaction.js +0 -157
- package/lib/pay/index.js +0 -57
- package/lib/pay/parseURL.js +0 -120
- package/lib/pay/prepareSendData.js +0 -38
- package/lib/pay/validateBeforePay.js +0 -44
- package/lib/tx-log/index.js +0 -18
- package/lib/tx-log/solana-monitor.js +0 -354
- package/src/__tests__/api.test.js +0 -286
- package/src/__tests__/assets.js +0 -7
- package/src/__tests__/fixtures.js +0 -3166
- package/src/__tests__/index.test.js +0 -7
- package/src/__tests__/staking.test.js +0 -85
- package/src/__tests__/token.test.js +0 -374
- package/src/__tests__/ws.test.js +0 -74
- package/src/tx-log/__tests__/solana-monitor-api-mock.js +0 -353
- package/src/tx-log/__tests__/solana-monitor.integration.test.js +0 -119
- package/src/tx-log/__tests__/solana-monitor.test.js +0 -132
|
@@ -1,44 +0,0 @@
|
|
|
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
|
-
}
|
package/lib/tx-log/index.js
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,354 +0,0 @@
|
|
|
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;
|
|
@@ -1,286 +0,0 @@
|
|
|
1
|
-
import lodash from 'lodash'
|
|
2
|
-
import { fetch } from '@exodus/fetch'
|
|
3
|
-
import wretch from 'wretch'
|
|
4
|
-
import * as splToken from '@exodus/solana-spl-token'
|
|
5
|
-
import {
|
|
6
|
-
generateKeyPair,
|
|
7
|
-
PublicKey,
|
|
8
|
-
LAMPORTS_PER_SOL,
|
|
9
|
-
TOKEN_PROGRAM_ID,
|
|
10
|
-
SystemProgram,
|
|
11
|
-
U64,
|
|
12
|
-
SolanaWeb3Transaction as Transaction,
|
|
13
|
-
} from '@exodus/solana-lib'
|
|
14
|
-
import { Api } from '../index'
|
|
15
|
-
import { SOL_TRANSFER_TX } from './fixtures'
|
|
16
|
-
import assets from './assets'
|
|
17
|
-
|
|
18
|
-
const api = new Api({ assets })
|
|
19
|
-
|
|
20
|
-
wretch().polyfills({
|
|
21
|
-
fetch,
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
const SECONDS = 1000
|
|
25
|
-
jest.setTimeout(60 * SECONDS)
|
|
26
|
-
|
|
27
|
-
const ADDRESS_WITH_HISTORY = 'Cd1uLoQwGZiDSyJxrwbpYmuDZM2tDftkowPjWv5YE5Z7'
|
|
28
|
-
const ADDRESS_FRESH = '3bbnZt1mzp1EBRR9Rm6TX2cXzZYbVvM9THutWL8bCmVH' // no txs history
|
|
29
|
-
const TXID =
|
|
30
|
-
'4AwqVmXFzciF89JyzMrTr5FT2tX1iMg7ypzCzUsGMD54dwjrE8w4GLNTbXXRpNcE8JkTSGRbZ8SzEc5umQJNFS3m'
|
|
31
|
-
const SRM_TOKEN_TXID =
|
|
32
|
-
'2zJ7dnXMZYUhw4uaeLaBRcoDsgfM9HZ1GSbfjJp4SxWrFhzEdwNKFfFFJ8LL6ckKwh5Xg5ik5pUuRtjMkGMDZpgV'
|
|
33
|
-
|
|
34
|
-
beforeAll(() => {
|
|
35
|
-
return api.watchAddress({ address: ADDRESS_WITH_HISTORY }) // Open WS connection
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
test('Solana: getRecentBlockHash', async () => {
|
|
39
|
-
const blockHash = await api.getRecentBlockHash()
|
|
40
|
-
expect(typeof blockHash).toEqual('string')
|
|
41
|
-
expect(blockHash.length > 40).toEqual(true)
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
test('Solana: getTransactionById', async () => {
|
|
45
|
-
const result = await api.getTransactionById(TXID)
|
|
46
|
-
expect(result.transaction.signatures[0]).toEqual(TXID) // string
|
|
47
|
-
expect(result.meta.fee > 0).toEqual(true)
|
|
48
|
-
expect(Array.isArray(result.meta.preBalances)).toEqual(true)
|
|
49
|
-
expect(Array.isArray(result.meta.postBalances)).toEqual(true)
|
|
50
|
-
expect(result.transaction.message.accountKeys.length > 1).toEqual(true)
|
|
51
|
-
expect(result.transaction.message.accountKeys[0].pubkey).toEqual(
|
|
52
|
-
'Bnjx2F242ZK6LsKbAvRwDoeKczJTK8LFM918p8sZCb69'
|
|
53
|
-
)
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
test('Solana: getFee', async () => {
|
|
57
|
-
const fee = await api.getFee()
|
|
58
|
-
expect(fee >= 4000).toEqual(true) // usually 5000 lamports
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
test('Solana: getBalance', async () => {
|
|
62
|
-
const [balance1, balance2] = await Promise.all([
|
|
63
|
-
api.getBalance(ADDRESS_WITH_HISTORY),
|
|
64
|
-
api.getBalance(ADDRESS_FRESH),
|
|
65
|
-
])
|
|
66
|
-
expect(typeof balance1 === 'number').toEqual(true)
|
|
67
|
-
expect(typeof balance2 === 'number').toEqual(true)
|
|
68
|
-
expect(balance2).toEqual(0)
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
test('Solana: getBlockTime', async () => {
|
|
72
|
-
const ts = await api.getBlockTime(49901505)
|
|
73
|
-
expect(ts) // if null the backend started pruning old tx entries and has partial ledger!
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
test('Solana: getConfirmedSignaturesForAddress', async () => {
|
|
77
|
-
const addrTxs = await api.getConfirmedSignaturesForAddress(ADDRESS_WITH_HISTORY)
|
|
78
|
-
expect(addrTxs.length > 4)
|
|
79
|
-
const addrTxs2 = await api.getConfirmedSignaturesForAddress(ADDRESS_WITH_HISTORY, { until: '' })
|
|
80
|
-
expect(addrTxs2.length > 4).toEqual(true)
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
describe('getTransactions', () => {
|
|
84
|
-
test('Solana: Returns empty transactions if account is not created', async () => {
|
|
85
|
-
const { transactions } = await api.getTransactions(ADDRESS_FRESH)
|
|
86
|
-
expect(transactions.length === 0).toBeTruthy()
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
test('Solana: Returns correct transactions if account is created', async () => {
|
|
90
|
-
const { transactions, newCursor } = await api.getTransactions(ADDRESS_WITH_HISTORY)
|
|
91
|
-
expect(transactions.length >= 1).toBeTruthy()
|
|
92
|
-
expect(typeof newCursor).toEqual('string')
|
|
93
|
-
// check tokens txs returned as well
|
|
94
|
-
const tokenTx = transactions.find(({ id }) => id === SRM_TOKEN_TXID)
|
|
95
|
-
// console.log('tokenTx:', tokenTx)
|
|
96
|
-
expect(tokenTx.timestamp).toEqual(1608598478000)
|
|
97
|
-
expect(tokenTx.id).toEqual(SRM_TOKEN_TXID)
|
|
98
|
-
expect(tokenTx.token.tokenName).toEqual('serum')
|
|
99
|
-
expect(tokenTx.owner).toEqual(null)
|
|
100
|
-
expect(tokenTx.from).toEqual('71GbXnJkHz15kzjagB2f7N9H7HPA99v6BFuqfdYL5qtf')
|
|
101
|
-
expect(tokenTx.amount).toEqual(1000)
|
|
102
|
-
expect(tokenTx.fee).toEqual(0)
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
test('Solana: Returns expected transactions with cursor', async () => {
|
|
106
|
-
const OLD_TXID =
|
|
107
|
-
'3qFUz73ZPiexM8yHyTFhnEhXgVinzXCQMieJbTsfZKcW5V3vPSJJvRCSKjCHJDYtWqReeBrFV98hcCFaAznpjgUC' // existing old txid that should not be returned in txs history after cursor date.
|
|
108
|
-
const { transactions } = await api.getTransactions(ADDRESS_WITH_HISTORY, {
|
|
109
|
-
cursor:
|
|
110
|
-
'4kzJjAbaT1PVSuk7JDh6697Y63MrGGMwDgi8CKRtctHbXWRjPtrzY1RnbMuedPDefru2hrGeLfRDpqtdFmQQAMXm',
|
|
111
|
-
})
|
|
112
|
-
expect(transactions.length >= 1).toBeTruthy()
|
|
113
|
-
expect(lodash.find(transactions, { id: OLD_TXID })).toBeFalsy()
|
|
114
|
-
expect(
|
|
115
|
-
lodash.find(transactions, {
|
|
116
|
-
id:
|
|
117
|
-
'3V7eDr5BbgZfgiADpUxM2fHQ1ZAoJHddxxfFRd8KtDQeTqLsv9wkfeg6NrVHzQc7wnEmVRQPdsYBT3RKLztL6Suf',
|
|
118
|
-
})
|
|
119
|
-
).toBeTruthy()
|
|
120
|
-
})
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
test('Solana: parseTransaction on a SOL tx', async () => {
|
|
124
|
-
const owner = 'Bnjx2F242ZK6LsKbAvRwDoeKczJTK8LFM918p8sZCb69'
|
|
125
|
-
const tx = await api.parseTransaction(owner, SOL_TRANSFER_TX)
|
|
126
|
-
// console.log(tx)
|
|
127
|
-
expect(tx.id).toEqual(
|
|
128
|
-
'4AwqVmXFzciF89JyzMrTr5FT2tX1iMg7ypzCzUsGMD54dwjrE8w4GLNTbXXRpNcE8JkTSGRbZ8SzEc5umQJNFS3m'
|
|
129
|
-
)
|
|
130
|
-
expect(tx.slot).toEqual(36446581)
|
|
131
|
-
expect(tx.error).toEqual(false)
|
|
132
|
-
expect(tx.from).toEqual('Bnjx2F242ZK6LsKbAvRwDoeKczJTK8LFM918p8sZCb69')
|
|
133
|
-
expect(tx.to).toEqual('84HuMpN8JPy9DLerT5nRcEHjkb1UQS3TD4CvR9y8MqgD')
|
|
134
|
-
expect(tx.amount).toEqual(2e13)
|
|
135
|
-
expect(tx.fee).toEqual(5e3)
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
test('Solana: getMinimumBalanceForRentExemption', async () => {
|
|
139
|
-
const minimumBalance = await api.getMinimumBalanceForRentExemption(80)
|
|
140
|
-
expect(typeof minimumBalance === 'number').toEqual(true)
|
|
141
|
-
})
|
|
142
|
-
|
|
143
|
-
test('Solana: getMetaplexMetadata', async () => {
|
|
144
|
-
const MANGO_SPL_TOKEN = 'MangoCzJ36AjZyKwVj3VnYU4GTonjfVEnJmvvWaxLac'
|
|
145
|
-
|
|
146
|
-
const metadata = await api.getMetaplexMetadata(MANGO_SPL_TOKEN)
|
|
147
|
-
// console.log('metadata', metadata)
|
|
148
|
-
expect(metadata.name).toEqual('Mango')
|
|
149
|
-
expect(metadata.symbol).toEqual('MNGO')
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
test('Solana: getMetaplexMetadata of invalid address returns null', async () => {
|
|
153
|
-
const RANDOM_SPL_TOKEN = 'Dhg9XnzJWzSQqH2aAnhPTEJHGQAkALDfD98MA499A7pa' // doesn't have metaplex data
|
|
154
|
-
|
|
155
|
-
const metadata = await api.getMetaplexMetadata(RANDOM_SPL_TOKEN)
|
|
156
|
-
expect(metadata).toEqual(null)
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
test('Solana: broadcastTransaction', async () => {
|
|
160
|
-
// execute setTimeout callback immediately (no wait)
|
|
161
|
-
jest.spyOn(global, 'setTimeout')
|
|
162
|
-
global.setTimeout.mockImplementation((callback) => {
|
|
163
|
-
return callback()
|
|
164
|
-
})
|
|
165
|
-
|
|
166
|
-
try {
|
|
167
|
-
// send transaction
|
|
168
|
-
const signedTx =
|
|
169
|
-
'AVXo5X7UNzpuOmYzkZ+fqHDGiRLTSMlWlUCcZKzEV5CIKlrdvZa3/2GrJJfPrXgZqJbYDaGiOnP99tI/sRJfiwwBAAEDRQ/n5E5CLbMbHanUG3+iVvBAWZu0WFM6NoB5xfybQ7kNwwgfIhv6odn2qTUu/gOisDtaeCW1qlwW/gx3ccr/4wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAvsInicc+E3IZzLqeA+iM5cn9kSaeFzOuClz1Z2kZQy0BAgIAAQwCAAAAAPIFKgEAAAA='
|
|
170
|
-
await api.broadcastTransaction(signedTx)
|
|
171
|
-
expect('To Fail').toEqual(true)
|
|
172
|
-
} catch (err) {
|
|
173
|
-
expect(err.message.match(/Blockhash not found/)).toBeTruthy()
|
|
174
|
-
} finally {
|
|
175
|
-
global.setTimeout.mockRestore()
|
|
176
|
-
}
|
|
177
|
-
})
|
|
178
|
-
|
|
179
|
-
describe('Solana: getDecimals', () => {
|
|
180
|
-
test('returns decimals if valid SPL address', async () => {
|
|
181
|
-
const decimals = await api.getDecimals('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v') // USDC
|
|
182
|
-
expect(decimals).toEqual(6)
|
|
183
|
-
})
|
|
184
|
-
|
|
185
|
-
test('Solana: getDecimals: errors if invalid SPL address', async () => {
|
|
186
|
-
try {
|
|
187
|
-
await api.getDecimals('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1w')
|
|
188
|
-
expect('To Fail').toEqual(true)
|
|
189
|
-
} catch (err) {
|
|
190
|
-
expect(err.message.match(/could not find account/)).toBeTruthy()
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
try {
|
|
194
|
-
await api.getDecimals('EPjFWdd5ZwTDt1')
|
|
195
|
-
expect('To Fail').toEqual(true)
|
|
196
|
-
} catch (err) {
|
|
197
|
-
expect(err.message.match(/WrongSize/)).toBeTruthy()
|
|
198
|
-
}
|
|
199
|
-
})
|
|
200
|
-
})
|
|
201
|
-
|
|
202
|
-
describe('Solana: getAccountInfo', () => {
|
|
203
|
-
test('returns parsed data', async () => {
|
|
204
|
-
const value = await api.getAccountInfo('26EA6FxEfryse9iJbZrtYCUwhAwxmqG1GurrfbJarmem')
|
|
205
|
-
expect(typeof value?.data?.parsed === 'object').toBeTruthy()
|
|
206
|
-
})
|
|
207
|
-
|
|
208
|
-
test('returns base64 data', async () => {
|
|
209
|
-
const value = await api.getAccountInfo('26EA6FxEfryse9iJbZrtYCUwhAwxmqG1GurrfbJarmem', 'base64')
|
|
210
|
-
expect(Array.isArray(value.data)).toBeTruthy()
|
|
211
|
-
expect(Buffer.from(value.data[0], 'base64').length).toEqual(8184)
|
|
212
|
-
})
|
|
213
|
-
})
|
|
214
|
-
|
|
215
|
-
describe('Solana: isSpl', () => {
|
|
216
|
-
test('returns true for addresses owned by the Token Program', async () => {
|
|
217
|
-
expect(await api.isSpl('BmxZ1pghpcoyT7aykj7D1o4AxWirTqvD7zD2tNngjirT')).toBeTruthy()
|
|
218
|
-
expect(await api.isSpl('buMnhMd5xSyXBssTQo15jouu8VhuEZJCfbtBUZgRcuW')).toBeTruthy()
|
|
219
|
-
expect(await api.isSpl('MAPS41MDahZ9QdKXhVa4dWB9RuyfV4XqhyAZ8XcYepb')).toBeTruthy()
|
|
220
|
-
})
|
|
221
|
-
|
|
222
|
-
test('returns false for addresses not owned by the Token Program', async () => {
|
|
223
|
-
expect(await api.isSpl('9zyPU1mjgzaVyQsYwKJJ7AhVz5bgx5uc1NPABvAcUXsT')).toBeFalsy()
|
|
224
|
-
})
|
|
225
|
-
})
|
|
226
|
-
|
|
227
|
-
describe('Solana: simulateAndRetrieveSideEffects', () => {
|
|
228
|
-
const ownerSolAccount = new PublicKey('3b1rT3knyRnZHKHCCxKWnJDWo8k6SDdSTvYzqyXUYwxL') // 1 SOL
|
|
229
|
-
const ownerTokenAddress = new PublicKey('FRaWiAGc3bvQKAkqqAxwmLvCVH7AaUGo6MrBUYhCmojK') // 0 Wrapped SOL (Token)
|
|
230
|
-
|
|
231
|
-
const secondarySolAccount = new PublicKey('41TDiDgCLz2eh5vPMhWCdNeaoMfrs3tM4rGdZgX4jL56') // 1 SOL
|
|
232
|
-
const secondaryTokenAddress = new PublicKey('8HPbjj4QVnYGUcRrfeZ3XdMhq36RnaJqvUHjHZYZ2p65') // 1 Wrapped SOL (Token)
|
|
233
|
-
|
|
234
|
-
// Use solana devnet since we need actual accounts with available balance for simulation
|
|
235
|
-
const testApi = new Api({ rpcUrl: 'https://api.devnet.solana.com', assets })
|
|
236
|
-
|
|
237
|
-
test('returns willSend and willReceive account changes', async () => {
|
|
238
|
-
const SOL_SEND_AMOUNT = LAMPORTS_PER_SOL * 0.1
|
|
239
|
-
const TOKEN_SEND_AMOUNT = LAMPORTS_PER_SOL * 0.531
|
|
240
|
-
|
|
241
|
-
const txFee = await api.getFee()
|
|
242
|
-
const transaction = new Transaction()
|
|
243
|
-
// Owner sends SOL
|
|
244
|
-
transaction.add(
|
|
245
|
-
SystemProgram.transfer({
|
|
246
|
-
fromPubkey: ownerSolAccount,
|
|
247
|
-
toPubkey: generateKeyPair().publicKey,
|
|
248
|
-
lamports: SOL_SEND_AMOUNT,
|
|
249
|
-
})
|
|
250
|
-
)
|
|
251
|
-
// Owner receives Wrapped SOL token
|
|
252
|
-
transaction.add(
|
|
253
|
-
splToken.Token.createTransferInstruction(
|
|
254
|
-
TOKEN_PROGRAM_ID,
|
|
255
|
-
secondaryTokenAddress,
|
|
256
|
-
ownerTokenAddress,
|
|
257
|
-
secondarySolAccount,
|
|
258
|
-
[],
|
|
259
|
-
TOKEN_SEND_AMOUNT
|
|
260
|
-
)
|
|
261
|
-
)
|
|
262
|
-
transaction.recentBlockhash = await testApi.getRecentBlockHash()
|
|
263
|
-
transaction.feePayer = ownerSolAccount
|
|
264
|
-
transaction.signatures = [
|
|
265
|
-
{ signature: null, publicKey: ownerSolAccount },
|
|
266
|
-
{ signature: null, publicKey: secondarySolAccount },
|
|
267
|
-
]
|
|
268
|
-
|
|
269
|
-
const result = await testApi.simulateAndRetrieveSideEffects(
|
|
270
|
-
transaction.compileMessage(),
|
|
271
|
-
ownerSolAccount.toString()
|
|
272
|
-
)
|
|
273
|
-
|
|
274
|
-
expect(result.willSend.length === 1).toBeTruthy()
|
|
275
|
-
expect(result.willSend[0].type).toEqual('SOL')
|
|
276
|
-
expect(result.willSend[0].balance).toEqual(new U64((SOL_SEND_AMOUNT + txFee * 2) * -1)) // two tx and will send would be negative
|
|
277
|
-
|
|
278
|
-
expect(result.willReceive.length === 1).toBeTruthy()
|
|
279
|
-
expect(result.willReceive[0].type).toEqual('TOKEN')
|
|
280
|
-
expect(result.willReceive[0].balance).toEqual(new U64(TOKEN_SEND_AMOUNT))
|
|
281
|
-
})
|
|
282
|
-
})
|
|
283
|
-
|
|
284
|
-
afterAll(async () => {
|
|
285
|
-
return api.unwatchAddress({ address: ADDRESS_WITH_HISTORY })
|
|
286
|
-
})
|
package/src/__tests__/assets.js
DELETED