@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
package/lib/api.js
DELETED
|
@@ -1,1161 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
Object.defineProperty(exports, '__esModule', {
|
|
4
|
-
value: true,
|
|
5
|
-
})
|
|
6
|
-
exports.Api = void 0
|
|
7
|
-
|
|
8
|
-
var _bn = _interopRequireDefault(require('bn.js'))
|
|
9
|
-
|
|
10
|
-
var _assetJsonRpc = _interopRequireDefault(require('@exodus/asset-json-rpc'))
|
|
11
|
-
|
|
12
|
-
var _simpleRetry = require('@exodus/simple-retry')
|
|
13
|
-
|
|
14
|
-
var _fetch = require('@exodus/fetch')
|
|
15
|
-
|
|
16
|
-
var _solanaLib = require('@exodus/solana-lib')
|
|
17
|
-
|
|
18
|
-
var _assets = _interopRequireDefault(require('@exodus/assets'))
|
|
19
|
-
|
|
20
|
-
var _assert = _interopRequireDefault(require('assert'))
|
|
21
|
-
|
|
22
|
-
var _lodash = _interopRequireDefault(require('lodash'))
|
|
23
|
-
|
|
24
|
-
var _urlJoin = _interopRequireDefault(require('url-join'))
|
|
25
|
-
|
|
26
|
-
var _wretch = _interopRequireWildcard(require('wretch'))
|
|
27
|
-
|
|
28
|
-
var _nftsCore = require('@exodus/nfts-core')
|
|
29
|
-
|
|
30
|
-
var _connection = require('./connection')
|
|
31
|
-
|
|
32
|
-
function _getRequireWildcardCache() {
|
|
33
|
-
if (typeof WeakMap !== 'function') return null
|
|
34
|
-
var cache = new WeakMap()
|
|
35
|
-
_getRequireWildcardCache = function() {
|
|
36
|
-
return cache
|
|
37
|
-
}
|
|
38
|
-
return cache
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function _interopRequireWildcard(obj) {
|
|
42
|
-
if (obj && obj.__esModule) {
|
|
43
|
-
return obj
|
|
44
|
-
}
|
|
45
|
-
if (obj === null || (typeof obj !== 'object' && typeof obj !== 'function')) {
|
|
46
|
-
return { default: obj }
|
|
47
|
-
}
|
|
48
|
-
var cache = _getRequireWildcardCache()
|
|
49
|
-
if (cache && cache.has(obj)) {
|
|
50
|
-
return cache.get(obj)
|
|
51
|
-
}
|
|
52
|
-
var newObj = {}
|
|
53
|
-
var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor
|
|
54
|
-
for (var key in obj) {
|
|
55
|
-
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
56
|
-
var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null
|
|
57
|
-
if (desc && (desc.get || desc.set)) {
|
|
58
|
-
Object.defineProperty(newObj, key, desc)
|
|
59
|
-
} else {
|
|
60
|
-
newObj[key] = obj[key]
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
newObj.default = obj
|
|
65
|
-
if (cache) {
|
|
66
|
-
cache.set(obj, newObj)
|
|
67
|
-
}
|
|
68
|
-
return newObj
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function _interopRequireDefault(obj) {
|
|
72
|
-
return obj && obj.__esModule ? obj : { default: obj }
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const RPC_URL = 'https://solana.a.exodus.io' // https://vip-api.mainnet-beta.solana.com/, https://api.mainnet-beta.solana.com, https://solana-api.projectserum.com
|
|
76
|
-
|
|
77
|
-
const WS_ENDPOINT = 'wss://solana.a.exodus.io/ws' // Tokens + SOL api support
|
|
78
|
-
|
|
79
|
-
class Api {
|
|
80
|
-
constructor(rpcUrl, wsUrl) {
|
|
81
|
-
this.broadcastTransaction = async (signedTx, options) => {
|
|
82
|
-
console.log('Solana broadcasting TX:', signedTx) // base64
|
|
83
|
-
|
|
84
|
-
const defaultOptions = {
|
|
85
|
-
encoding: 'base64',
|
|
86
|
-
preflightCommitment: 'finalized',
|
|
87
|
-
}
|
|
88
|
-
const params = [signedTx, { ...defaultOptions, ...options }]
|
|
89
|
-
const errorMessagesToRetry = ['Blockhash not found']
|
|
90
|
-
const broadcastTxWithRetry = (0, _simpleRetry.retry)(
|
|
91
|
-
async () => {
|
|
92
|
-
try {
|
|
93
|
-
const result = await this.rpcCall('sendTransaction', params, {
|
|
94
|
-
forceHttp: true,
|
|
95
|
-
})
|
|
96
|
-
console.log(`tx ${JSON.stringify(result)} sent!`)
|
|
97
|
-
return result || null
|
|
98
|
-
} catch (error) {
|
|
99
|
-
if (
|
|
100
|
-
error.message &&
|
|
101
|
-
!errorMessagesToRetry.find((errorMessage) => error.message.includes(errorMessage))
|
|
102
|
-
) {
|
|
103
|
-
error.finalError = true
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
console.warn(`Error broadcasting tx. Retrying...`, error)
|
|
107
|
-
throw error
|
|
108
|
-
}
|
|
109
|
-
},
|
|
110
|
-
{
|
|
111
|
-
delayTimesMs: ['6s', '6s', '8s', '10s'],
|
|
112
|
-
}
|
|
113
|
-
)
|
|
114
|
-
return broadcastTxWithRetry()
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
this.simulateTransaction = async (encodedTransaction, options) => {
|
|
118
|
-
const {
|
|
119
|
-
value: { accounts },
|
|
120
|
-
} = await this.rpcCall('simulateTransaction', [encodedTransaction, options])
|
|
121
|
-
return accounts
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
this.resolveSimulationSideEffects = async (solAccounts, tokenAccounts) => {
|
|
125
|
-
const willReceive = []
|
|
126
|
-
const willSend = []
|
|
127
|
-
const resolveSols = solAccounts.map(async (account) => {
|
|
128
|
-
const currentAmount = await this.getBalance(account.address)
|
|
129
|
-
const balance = (0, _solanaLib.computeBalance)(account.amount, currentAmount)
|
|
130
|
-
return {
|
|
131
|
-
name: 'SOL',
|
|
132
|
-
symbol: 'SOL',
|
|
133
|
-
balance,
|
|
134
|
-
decimal: _solanaLib.SOL_DECIMAL,
|
|
135
|
-
type: 'SOL',
|
|
136
|
-
}
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
const _wrapAndHandleAccountNotFound = (fn, defaultValue) => {
|
|
140
|
-
return async (...params) => {
|
|
141
|
-
try {
|
|
142
|
-
return await fn.apply(this, params)
|
|
143
|
-
} catch (error) {
|
|
144
|
-
if (error.message && error.message.includes('could not find account')) {
|
|
145
|
-
return defaultValue
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
throw error
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const _getTokenBalance = _wrapAndHandleAccountNotFound(this.getTokenBalance, '0')
|
|
154
|
-
|
|
155
|
-
const _getDecimals = _wrapAndHandleAccountNotFound(this.getDecimals, 0)
|
|
156
|
-
|
|
157
|
-
const _getSupply = _wrapAndHandleAccountNotFound(this.getSupply, '0')
|
|
158
|
-
|
|
159
|
-
const resolveTokens = tokenAccounts.map(async (account) => {
|
|
160
|
-
try {
|
|
161
|
-
const [_tokenMetaPlex, currentAmount, decimal] = await Promise.all([
|
|
162
|
-
this.getMetaplexMetadata(account.mint),
|
|
163
|
-
_getTokenBalance(account.address),
|
|
164
|
-
_getDecimals(account.mint),
|
|
165
|
-
])
|
|
166
|
-
const tokenMetaPlex = _tokenMetaPlex || {
|
|
167
|
-
name: null,
|
|
168
|
-
symbol: null,
|
|
169
|
-
}
|
|
170
|
-
let nft = {
|
|
171
|
-
collectionId: null,
|
|
172
|
-
collectionName: null,
|
|
173
|
-
collectionTitle: null,
|
|
174
|
-
title: null,
|
|
175
|
-
} // Only perform an NFT check (getSupply) if decimal is zero
|
|
176
|
-
|
|
177
|
-
if (decimal === 0 && (await _getSupply(account.mint)) === '1') {
|
|
178
|
-
try {
|
|
179
|
-
const {
|
|
180
|
-
id: collectionId,
|
|
181
|
-
collectionName,
|
|
182
|
-
collectionTitle,
|
|
183
|
-
title,
|
|
184
|
-
} = await _nftsCore.magicEden.api.getNFTByMintAddress(account.mint)
|
|
185
|
-
nft = {
|
|
186
|
-
collectionId,
|
|
187
|
-
collectionTitle,
|
|
188
|
-
collectionName,
|
|
189
|
-
title,
|
|
190
|
-
}
|
|
191
|
-
tokenMetaPlex.name = tokenMetaPlex.name || collectionTitle
|
|
192
|
-
tokenMetaPlex.symbol = tokenMetaPlex.symbol || collectionName
|
|
193
|
-
} catch (error) {
|
|
194
|
-
console.warn(error)
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
const balance = (0, _solanaLib.computeBalance)(account.amount, currentAmount)
|
|
199
|
-
return {
|
|
200
|
-
balance,
|
|
201
|
-
decimal,
|
|
202
|
-
nft,
|
|
203
|
-
address: account.address,
|
|
204
|
-
mint: account.mint,
|
|
205
|
-
name: tokenMetaPlex.name,
|
|
206
|
-
symbol: tokenMetaPlex.symbol,
|
|
207
|
-
type: 'TOKEN',
|
|
208
|
-
}
|
|
209
|
-
} catch (error) {
|
|
210
|
-
console.warn(error)
|
|
211
|
-
return {
|
|
212
|
-
balance: null,
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
})
|
|
216
|
-
const accounts = await Promise.all([...resolveSols, ...resolveTokens])
|
|
217
|
-
accounts.forEach((account) => {
|
|
218
|
-
if (account.balance === null) {
|
|
219
|
-
return
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
if (account.balance > 0) {
|
|
223
|
-
willReceive.push(account)
|
|
224
|
-
} else {
|
|
225
|
-
willSend.push(account)
|
|
226
|
-
}
|
|
227
|
-
})
|
|
228
|
-
return {
|
|
229
|
-
willReceive,
|
|
230
|
-
willSend,
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
this.simulateAndRetrieveSideEffects = async (message, publicKey, transactionMessage) => {
|
|
235
|
-
const { config, accountAddresses } = (0, _solanaLib.getTransactionSimulationParams)(
|
|
236
|
-
transactionMessage || message
|
|
237
|
-
)
|
|
238
|
-
const signatures = new Array(message.header.numRequiredSignatures || 1).fill(null)
|
|
239
|
-
const encodedTransaction = (0, _solanaLib.buildRawTransaction)(
|
|
240
|
-
Buffer.from(message.serialize()),
|
|
241
|
-
signatures
|
|
242
|
-
).toString('base64')
|
|
243
|
-
const futureAccountsState = await this.simulateTransaction(encodedTransaction, config)
|
|
244
|
-
const { solAccounts, tokenAccounts } = (0, _solanaLib.filterAccountsByOwner)(
|
|
245
|
-
futureAccountsState,
|
|
246
|
-
accountAddresses,
|
|
247
|
-
publicKey
|
|
248
|
-
)
|
|
249
|
-
return this.resolveSimulationSideEffects(solAccounts, tokenAccounts)
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
this.simulateTransactionV1 = async (network, transactions, publicKey, origin) => {
|
|
253
|
-
;(0, _assert.default)(
|
|
254
|
-
['mainnet', 'devnet', 'testnet'].includes(network),
|
|
255
|
-
'invalid network provided'
|
|
256
|
-
)
|
|
257
|
-
;(0, _assert.default)(Array.isArray(transactions), 'transactions was not an array')
|
|
258
|
-
;(0, _assert.default)(typeof origin === 'string', 'origin must be a string')
|
|
259
|
-
const body = JSON.stringify({
|
|
260
|
-
transactions,
|
|
261
|
-
userAccount: publicKey,
|
|
262
|
-
metadata: {
|
|
263
|
-
origin,
|
|
264
|
-
},
|
|
265
|
-
})
|
|
266
|
-
const response = await (0, _fetch.fetch)(
|
|
267
|
-
`https://api.blowfish.xyz/solana/v0/${network}/scan/transactions`,
|
|
268
|
-
{
|
|
269
|
-
method: 'POST',
|
|
270
|
-
headers: {
|
|
271
|
-
'Content-Type': 'application/json',
|
|
272
|
-
'X-Api-Key': 'afd98abb-c464-413a-8947-6c29e6b6e393',
|
|
273
|
-
},
|
|
274
|
-
body,
|
|
275
|
-
}
|
|
276
|
-
)
|
|
277
|
-
|
|
278
|
-
if (!response.ok) {
|
|
279
|
-
throw new Error(await response.text())
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
return response.json()
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
this.setServer(rpcUrl)
|
|
286
|
-
this.setWsEndpoint(wsUrl)
|
|
287
|
-
this.setTokens(_assets.default)
|
|
288
|
-
this.tokensToSkip = {}
|
|
289
|
-
this.connections = {}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
setServer(rpcUrl) {
|
|
293
|
-
this.rpcUrl = rpcUrl || RPC_URL
|
|
294
|
-
this.api = (0, _assetJsonRpc.default)(this.rpcUrl)
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
setWsEndpoint(wsUrl) {
|
|
298
|
-
this.wsUrl = wsUrl || WS_ENDPOINT
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
setTokens(assets = {}) {
|
|
302
|
-
const solTokens = _lodash.default.pickBy(assets, (asset) => asset.assetType === 'SOLANA_TOKEN')
|
|
303
|
-
|
|
304
|
-
this.tokens = _lodash.default.mapKeys(solTokens, (v) => v.mintAddress)
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
request(path, contentType = 'application/json') {
|
|
308
|
-
return (0, _wretch.default)((0, _urlJoin.default)(this.rpcUrl, path)).headers({
|
|
309
|
-
'Content-type': contentType,
|
|
310
|
-
})
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
async watchAddress({
|
|
314
|
-
address,
|
|
315
|
-
tokensAddresses = [],
|
|
316
|
-
handleAccounts,
|
|
317
|
-
handleTransfers,
|
|
318
|
-
handleReconnect,
|
|
319
|
-
reconnectDelay,
|
|
320
|
-
}) {
|
|
321
|
-
const conn = new _connection.Connection({
|
|
322
|
-
endpoint: this.wsUrl,
|
|
323
|
-
address,
|
|
324
|
-
tokensAddresses,
|
|
325
|
-
callback: (updates) =>
|
|
326
|
-
this.handleUpdates({
|
|
327
|
-
updates,
|
|
328
|
-
address,
|
|
329
|
-
handleAccounts,
|
|
330
|
-
handleTransfers,
|
|
331
|
-
}),
|
|
332
|
-
reconnectCallback: handleReconnect,
|
|
333
|
-
reconnectDelay,
|
|
334
|
-
})
|
|
335
|
-
this.connections[address] = conn
|
|
336
|
-
return conn.start()
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
async unwatchAddress({ address }) {
|
|
340
|
-
if (this.connections[address]) {
|
|
341
|
-
await this.connections[address].stop()
|
|
342
|
-
delete this.connections[address]
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
async handleUpdates({ updates, address, handleAccounts, handleTransfers }) {
|
|
347
|
-
// console.log(`got ws updates from ${address}:`, updates)
|
|
348
|
-
if (handleTransfers) return handleTransfers(updates)
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
async rpcCall(method, params = [], { address = '', forceHttp = false } = {}) {
|
|
352
|
-
// ws request
|
|
353
|
-
const connection =
|
|
354
|
-
this.connections[address] || _lodash.default.sample(Object.values(this.connections)) // pick random connection
|
|
355
|
-
|
|
356
|
-
if (
|
|
357
|
-
_lodash.default.get(connection, 'isOpen') &&
|
|
358
|
-
!_lodash.default.get(connection, 'shutdown') &&
|
|
359
|
-
!forceHttp
|
|
360
|
-
) {
|
|
361
|
-
return connection.sendMessage(method, params)
|
|
362
|
-
} // http fallback
|
|
363
|
-
|
|
364
|
-
return this.api.post({
|
|
365
|
-
method,
|
|
366
|
-
params,
|
|
367
|
-
})
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
getTokenByAddress(mint) {
|
|
371
|
-
return this.tokens[mint]
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
isTokenSupported(mint) {
|
|
375
|
-
return !!this.getTokenByAddress(mint)
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
async getEpochInfo() {
|
|
379
|
-
const { epoch } = await this.rpcCall('getEpochInfo')
|
|
380
|
-
return Number(epoch)
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
async getStakeActivation(address) {
|
|
384
|
-
const { state } = await this.rpcCall('getStakeActivation', [address])
|
|
385
|
-
return state
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
async getRecentBlockHash(commitment) {
|
|
389
|
-
const result = await this.rpcCall(
|
|
390
|
-
'getRecentBlockhash',
|
|
391
|
-
[
|
|
392
|
-
{
|
|
393
|
-
commitment: commitment || 'finalized',
|
|
394
|
-
encoding: 'jsonParsed',
|
|
395
|
-
},
|
|
396
|
-
],
|
|
397
|
-
{
|
|
398
|
-
forceHttp: true,
|
|
399
|
-
}
|
|
400
|
-
)
|
|
401
|
-
return _lodash.default.get(result, 'value.blockhash')
|
|
402
|
-
} // Transaction structure: https://docs.solana.com/apps/jsonrpc-api#transaction-structure
|
|
403
|
-
|
|
404
|
-
async getTransactionById(id) {
|
|
405
|
-
return this.rpcCall('getTransaction', [
|
|
406
|
-
id,
|
|
407
|
-
{
|
|
408
|
-
encoding: 'jsonParsed',
|
|
409
|
-
maxSupportedTransactionVersion: 0,
|
|
410
|
-
},
|
|
411
|
-
])
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
async getFee() {
|
|
415
|
-
const result = await this.rpcCall('getRecentBlockhash', [
|
|
416
|
-
{
|
|
417
|
-
commitment: 'finalized',
|
|
418
|
-
encoding: 'jsonParsed',
|
|
419
|
-
},
|
|
420
|
-
])
|
|
421
|
-
return _lodash.default.get(result, 'value.feeCalculator.lamportsPerSignature')
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
async getBalance(address) {
|
|
425
|
-
const result = await this.rpcCall(
|
|
426
|
-
'getBalance',
|
|
427
|
-
[
|
|
428
|
-
address,
|
|
429
|
-
{
|
|
430
|
-
encoding: 'jsonParsed',
|
|
431
|
-
},
|
|
432
|
-
],
|
|
433
|
-
{
|
|
434
|
-
address,
|
|
435
|
-
}
|
|
436
|
-
)
|
|
437
|
-
return _lodash.default.get(result, 'value', 0)
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
async getBlockTime(slot) {
|
|
441
|
-
// might result in error if executed on a validator with partial ledger (https://github.com/solana-labs/solana/issues/12413)
|
|
442
|
-
return this.rpcCall('getBlockTime', [slot])
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
async getConfirmedSignaturesForAddress(address, { until, before, limit } = {}) {
|
|
446
|
-
until = until || undefined
|
|
447
|
-
return this.rpcCall(
|
|
448
|
-
'getSignaturesForAddress',
|
|
449
|
-
[
|
|
450
|
-
address,
|
|
451
|
-
{
|
|
452
|
-
until,
|
|
453
|
-
before,
|
|
454
|
-
limit,
|
|
455
|
-
},
|
|
456
|
-
],
|
|
457
|
-
{
|
|
458
|
-
address,
|
|
459
|
-
}
|
|
460
|
-
)
|
|
461
|
-
}
|
|
462
|
-
/**
|
|
463
|
-
* Get transactions from an address
|
|
464
|
-
*/
|
|
465
|
-
|
|
466
|
-
async getTransactions(address, { cursor, before, limit, includeUnparsed = false } = {}) {
|
|
467
|
-
let transactions = [] // cursor is a txHash
|
|
468
|
-
|
|
469
|
-
try {
|
|
470
|
-
let until = cursor
|
|
471
|
-
const tokenAccountsByOwner = await this.getTokenAccountsByOwner(address) // Array
|
|
472
|
-
|
|
473
|
-
const tokenAccountAddresses = tokenAccountsByOwner
|
|
474
|
-
.filter(({ tokenName }) => tokenName !== 'unknown')
|
|
475
|
-
.map(({ tokenAccountAddress }) => tokenAccountAddress)
|
|
476
|
-
const accountsToCheck = [address, ...tokenAccountAddresses]
|
|
477
|
-
const txsResultsByAccount = await Promise.all(
|
|
478
|
-
accountsToCheck.map((addr) =>
|
|
479
|
-
this.getConfirmedSignaturesForAddress(addr, {
|
|
480
|
-
until,
|
|
481
|
-
before,
|
|
482
|
-
limit,
|
|
483
|
-
})
|
|
484
|
-
)
|
|
485
|
-
)
|
|
486
|
-
let txsId = txsResultsByAccount.reduce((arr, row) => arr.concat(row), []) // merge arrays
|
|
487
|
-
|
|
488
|
-
txsId = _lodash.default.uniqBy(txsId, 'signature') // get txs details in parallel
|
|
489
|
-
|
|
490
|
-
const txsDetails = await Promise.all(txsId.map((tx) => this.getTransactionById(tx.signature)))
|
|
491
|
-
txsDetails.forEach((txDetail) => {
|
|
492
|
-
if (txDetail === null) return
|
|
493
|
-
const timestamp = txDetail.blockTime * 1000
|
|
494
|
-
const parsedTx = this.parseTransaction(address, txDetail, tokenAccountsByOwner, {
|
|
495
|
-
includeUnparsed,
|
|
496
|
-
})
|
|
497
|
-
if (!parsedTx.from && !includeUnparsed) return // cannot parse it
|
|
498
|
-
// split dexTx in separate txs
|
|
499
|
-
|
|
500
|
-
if (parsedTx.dexTxs) {
|
|
501
|
-
parsedTx.dexTxs.forEach((tx) => {
|
|
502
|
-
transactions.push({
|
|
503
|
-
timestamp,
|
|
504
|
-
date: new Date(timestamp),
|
|
505
|
-
...tx,
|
|
506
|
-
})
|
|
507
|
-
})
|
|
508
|
-
delete parsedTx.dexTxs
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
transactions.push({
|
|
512
|
-
timestamp,
|
|
513
|
-
date: new Date(timestamp),
|
|
514
|
-
...parsedTx,
|
|
515
|
-
})
|
|
516
|
-
})
|
|
517
|
-
} catch (err) {
|
|
518
|
-
console.warn('Solana error:', err)
|
|
519
|
-
throw err
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
transactions = _lodash.default.orderBy(transactions, ['timestamp'], ['desc'])
|
|
523
|
-
const newCursor = transactions[0] ? transactions[0].id : cursor
|
|
524
|
-
return {
|
|
525
|
-
transactions,
|
|
526
|
-
newCursor,
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
parseTransaction(
|
|
531
|
-
ownerAddress,
|
|
532
|
-
txDetails,
|
|
533
|
-
tokenAccountsByOwner,
|
|
534
|
-
{ includeUnparsed = false } = {}
|
|
535
|
-
) {
|
|
536
|
-
let {
|
|
537
|
-
fee,
|
|
538
|
-
preBalances,
|
|
539
|
-
postBalances,
|
|
540
|
-
preTokenBalances,
|
|
541
|
-
postTokenBalances,
|
|
542
|
-
innerInstructions,
|
|
543
|
-
} = txDetails.meta
|
|
544
|
-
preBalances = preBalances || []
|
|
545
|
-
postBalances = postBalances || []
|
|
546
|
-
preTokenBalances = preTokenBalances || []
|
|
547
|
-
postTokenBalances = postTokenBalances || []
|
|
548
|
-
innerInstructions = innerInstructions || []
|
|
549
|
-
let { instructions, accountKeys } = txDetails.transaction.message
|
|
550
|
-
|
|
551
|
-
const getUnparsedTx = () => {
|
|
552
|
-
const ownerIndex = accountKeys.findIndex((accountKey) => accountKey.pubkey === ownerAddress)
|
|
553
|
-
const feePaid = ownerIndex === 0 ? fee : 0
|
|
554
|
-
return {
|
|
555
|
-
unparsed: true,
|
|
556
|
-
amount:
|
|
557
|
-
ownerIndex === -1 ? 0 : postBalances[ownerIndex] - preBalances[ownerIndex] + feePaid,
|
|
558
|
-
fee: feePaid,
|
|
559
|
-
data: {
|
|
560
|
-
meta: txDetails.meta,
|
|
561
|
-
},
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
const getInnerTxsFromBalanceChanges = () => {
|
|
566
|
-
const ownPreTokenBalances = preTokenBalances.filter(
|
|
567
|
-
(balance) => balance.owner === ownerAddress
|
|
568
|
-
)
|
|
569
|
-
const ownPostTokenBalances = postTokenBalances.filter(
|
|
570
|
-
(balance) => balance.owner === ownerAddress
|
|
571
|
-
)
|
|
572
|
-
return ownPostTokenBalances
|
|
573
|
-
.map((postBalance) => {
|
|
574
|
-
const tokenAccount = tokenAccountsByOwner.find(
|
|
575
|
-
(tokenAccount) => tokenAccount.mintAddress === postBalance.mint
|
|
576
|
-
)
|
|
577
|
-
const preBalance = ownPreTokenBalances.find(
|
|
578
|
-
(balance) => balance.accountIndex === postBalance.accountIndex
|
|
579
|
-
)
|
|
580
|
-
const preAmount = new _bn.default(
|
|
581
|
-
_lodash.default.get(preBalance, 'uiTokenAmount.amount', '0'),
|
|
582
|
-
10
|
|
583
|
-
)
|
|
584
|
-
const postAmount = new _bn.default(
|
|
585
|
-
_lodash.default.get(postBalance, 'uiTokenAmount.amount', '0'),
|
|
586
|
-
10
|
|
587
|
-
)
|
|
588
|
-
const amount = postAmount.sub(preAmount)
|
|
589
|
-
if (!tokenAccount || amount.isZero()) return null // This is not perfect as there could be multiple same-token transfers in single
|
|
590
|
-
// transaction, but our wallet only supports one transaction with single txId
|
|
591
|
-
// so we are picking first that matches (correct token + type - send or receive)
|
|
592
|
-
|
|
593
|
-
const match = innerInstructions.find((inner) => {
|
|
594
|
-
const targetOwner = amount.isNeg() ? ownerAddress : null
|
|
595
|
-
return (
|
|
596
|
-
inner.token.mintAddress === tokenAccount.mintAddress && targetOwner === inner.owner
|
|
597
|
-
)
|
|
598
|
-
}) // It's possible we won't find a match, because our innerInstructions only contain
|
|
599
|
-
// spl-token transfers, but balances of SPL tokens can change in different ways too.
|
|
600
|
-
// for now, we are ignoring this to simplify as those cases are not that common, but
|
|
601
|
-
// they should be handled eventually. It was already a scretch to add unparsed txs logic
|
|
602
|
-
// to existing parser, expanding it further is not going to end well.
|
|
603
|
-
// this probably should be refactored from ground to handle all those transactions
|
|
604
|
-
// as a core part of it in the future
|
|
605
|
-
|
|
606
|
-
if (!match) return null
|
|
607
|
-
const { from, to, owner } = match
|
|
608
|
-
return {
|
|
609
|
-
id: txDetails.transaction.signatures[0],
|
|
610
|
-
slot: txDetails.slot,
|
|
611
|
-
owner,
|
|
612
|
-
from,
|
|
613
|
-
to,
|
|
614
|
-
amount: amount.abs().toString(),
|
|
615
|
-
// inconsistent with the rest, but it can and did overflow
|
|
616
|
-
fee: 0,
|
|
617
|
-
token: tokenAccount,
|
|
618
|
-
data: {
|
|
619
|
-
inner: true,
|
|
620
|
-
},
|
|
621
|
-
}
|
|
622
|
-
})
|
|
623
|
-
.filter((ix) => !!ix)
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
instructions = instructions
|
|
627
|
-
.filter((ix) => ix.parsed) // only known instructions
|
|
628
|
-
.map((ix) => ({
|
|
629
|
-
program: ix.program,
|
|
630
|
-
// system or spl-token
|
|
631
|
-
type: ix.parsed.type,
|
|
632
|
-
// transfer, createAccount, initializeAccount
|
|
633
|
-
...ix.parsed.info,
|
|
634
|
-
}))
|
|
635
|
-
innerInstructions = innerInstructions
|
|
636
|
-
.reduce((acc, val) => {
|
|
637
|
-
return acc.concat(val.instructions)
|
|
638
|
-
}, [])
|
|
639
|
-
.map((ix) => {
|
|
640
|
-
const type = _lodash.default.get(ix, 'parsed.type')
|
|
641
|
-
|
|
642
|
-
const isTransferTx = ix.parsed && ix.program === 'spl-token' && type === 'transfer'
|
|
643
|
-
|
|
644
|
-
const source = _lodash.default.get(ix, 'parsed.info.source')
|
|
645
|
-
|
|
646
|
-
const destination = _lodash.default.get(ix, 'parsed.info.destination')
|
|
647
|
-
|
|
648
|
-
const amount = Number(_lodash.default.get(ix, 'parsed.info.amount', 0))
|
|
649
|
-
const tokenAccount = tokenAccountsByOwner.find(({ tokenAccountAddress }) => {
|
|
650
|
-
return [source, destination].includes(tokenAccountAddress)
|
|
651
|
-
})
|
|
652
|
-
const isSending = !!tokenAccountsByOwner.find(({ tokenAccountAddress }) => {
|
|
653
|
-
return [source].includes(tokenAccountAddress)
|
|
654
|
-
}) // owner if it's a send tx
|
|
655
|
-
|
|
656
|
-
const instruction = {
|
|
657
|
-
id: txDetails.transaction.signatures[0],
|
|
658
|
-
slot: txDetails.slot,
|
|
659
|
-
owner: isSending ? ownerAddress : null,
|
|
660
|
-
from: source,
|
|
661
|
-
to: destination,
|
|
662
|
-
amount,
|
|
663
|
-
token: tokenAccount,
|
|
664
|
-
fee: isSending ? fee : 0,
|
|
665
|
-
}
|
|
666
|
-
return isTransferTx && tokenAccount ? instruction : null
|
|
667
|
-
})
|
|
668
|
-
.filter((ix) => !!ix) // program:type tells us if it's a SOL or Token transfer
|
|
669
|
-
|
|
670
|
-
const solanaTx = _lodash.default.find(instructions, (ix) => {
|
|
671
|
-
if (![ix.source, ix.destination].includes(ownerAddress)) return false
|
|
672
|
-
return ix.program === 'system' && ix.type === 'transfer'
|
|
673
|
-
}) // get SOL transfer
|
|
674
|
-
|
|
675
|
-
const stakeTx = _lodash.default.find(instructions, {
|
|
676
|
-
program: 'system',
|
|
677
|
-
type: 'createAccountWithSeed',
|
|
678
|
-
})
|
|
679
|
-
|
|
680
|
-
const stakeWithdraw = _lodash.default.find(instructions, {
|
|
681
|
-
program: 'stake',
|
|
682
|
-
type: 'withdraw',
|
|
683
|
-
})
|
|
684
|
-
|
|
685
|
-
const stakeUndelegate = _lodash.default.find(instructions, {
|
|
686
|
-
program: 'stake',
|
|
687
|
-
type: 'deactivate',
|
|
688
|
-
})
|
|
689
|
-
|
|
690
|
-
const hasSolanaTx = solanaTx && !preTokenBalances.length && !postTokenBalances.length // only SOL moved and no tokens movements
|
|
691
|
-
|
|
692
|
-
let tx = {}
|
|
693
|
-
|
|
694
|
-
if (hasSolanaTx) {
|
|
695
|
-
// Solana tx
|
|
696
|
-
const isSending = ownerAddress === solanaTx.source
|
|
697
|
-
tx = {
|
|
698
|
-
owner: solanaTx.source,
|
|
699
|
-
from: solanaTx.source,
|
|
700
|
-
to: solanaTx.destination,
|
|
701
|
-
amount: solanaTx.lamports,
|
|
702
|
-
// number
|
|
703
|
-
fee: isSending ? fee : 0,
|
|
704
|
-
}
|
|
705
|
-
} else if (stakeTx) {
|
|
706
|
-
// start staking
|
|
707
|
-
tx = {
|
|
708
|
-
owner: stakeTx.base,
|
|
709
|
-
from: stakeTx.base,
|
|
710
|
-
to: stakeTx.base,
|
|
711
|
-
amount: stakeTx.lamports,
|
|
712
|
-
fee,
|
|
713
|
-
staking: {
|
|
714
|
-
method: 'createAccountWithSeed',
|
|
715
|
-
seed: stakeTx.seed,
|
|
716
|
-
stakeAddresses: [stakeTx.newAccount],
|
|
717
|
-
stake: stakeTx.lamports,
|
|
718
|
-
},
|
|
719
|
-
}
|
|
720
|
-
} else if (stakeWithdraw) {
|
|
721
|
-
const stakeAccounts = _lodash.default.map(
|
|
722
|
-
_lodash.default.filter(instructions, {
|
|
723
|
-
program: 'stake',
|
|
724
|
-
type: 'withdraw',
|
|
725
|
-
}),
|
|
726
|
-
'stakeAccount'
|
|
727
|
-
)
|
|
728
|
-
|
|
729
|
-
tx = {
|
|
730
|
-
owner: stakeWithdraw.withdrawAuthority,
|
|
731
|
-
from: stakeWithdraw.stakeAccount,
|
|
732
|
-
to: stakeWithdraw.destination,
|
|
733
|
-
amount: stakeWithdraw.lamports,
|
|
734
|
-
fee,
|
|
735
|
-
staking: {
|
|
736
|
-
method: 'withdraw',
|
|
737
|
-
stakeAddresses: stakeAccounts,
|
|
738
|
-
stake: stakeWithdraw.lamports,
|
|
739
|
-
},
|
|
740
|
-
}
|
|
741
|
-
} else if (stakeUndelegate) {
|
|
742
|
-
const stakeAccounts = _lodash.default.map(
|
|
743
|
-
_lodash.default.filter(instructions, {
|
|
744
|
-
program: 'stake',
|
|
745
|
-
type: 'deactivate',
|
|
746
|
-
}),
|
|
747
|
-
'stakeAccount'
|
|
748
|
-
)
|
|
749
|
-
|
|
750
|
-
tx = {
|
|
751
|
-
owner: stakeUndelegate.stakeAuthority,
|
|
752
|
-
from: stakeUndelegate.stakeAuthority,
|
|
753
|
-
to: stakeUndelegate.stakeAccount,
|
|
754
|
-
// obsolete
|
|
755
|
-
amount: 0,
|
|
756
|
-
fee,
|
|
757
|
-
staking: {
|
|
758
|
-
method: 'undelegate',
|
|
759
|
-
stakeAddresses: stakeAccounts,
|
|
760
|
-
},
|
|
761
|
-
}
|
|
762
|
-
} else {
|
|
763
|
-
// Token tx
|
|
764
|
-
_assert.default.ok(
|
|
765
|
-
Array.isArray(tokenAccountsByOwner),
|
|
766
|
-
'tokenAccountsByOwner is required when parsing token tx'
|
|
767
|
-
)
|
|
768
|
-
|
|
769
|
-
let tokenTxs = _lodash.default
|
|
770
|
-
.filter(instructions, ({ program, type }) => {
|
|
771
|
-
return program === 'spl-token' && ['transfer', 'transferChecked'].includes(type)
|
|
772
|
-
}) // get Token transfer: could have more than 1 instructions
|
|
773
|
-
.map((ix) => {
|
|
774
|
-
// add token details based on source/destination address
|
|
775
|
-
let tokenAccount = _lodash.default.find(tokenAccountsByOwner, {
|
|
776
|
-
tokenAccountAddress: ix.source,
|
|
777
|
-
})
|
|
778
|
-
|
|
779
|
-
const isSending = !!tokenAccount
|
|
780
|
-
if (!isSending)
|
|
781
|
-
tokenAccount = _lodash.default.find(tokenAccountsByOwner, {
|
|
782
|
-
tokenAccountAddress: ix.destination,
|
|
783
|
-
}) // receiving
|
|
784
|
-
|
|
785
|
-
if (!tokenAccount) return null // no transfers with our addresses involved
|
|
786
|
-
|
|
787
|
-
const owner = isSending ? ownerAddress : null
|
|
788
|
-
delete tokenAccount.balance
|
|
789
|
-
delete tokenAccount.owner
|
|
790
|
-
return {
|
|
791
|
-
owner,
|
|
792
|
-
token: tokenAccount,
|
|
793
|
-
from: ix.source,
|
|
794
|
-
to: ix.destination,
|
|
795
|
-
amount: Number(ix.amount || _lodash.default.get(ix, 'tokenAmount.amount', 0)),
|
|
796
|
-
// supporting both types: transfer and transferChecked
|
|
797
|
-
fee: isSending ? fee : 0, // in lamports
|
|
798
|
-
}
|
|
799
|
-
})
|
|
800
|
-
|
|
801
|
-
if (tokenTxs.length) {
|
|
802
|
-
// found spl-token simple transfer/transferChecked instruction
|
|
803
|
-
// .reduce to sum/sub (based on isSending) all the same tokens amount (From instructions -> 1 single tx)
|
|
804
|
-
tx = tokenTxs.reduce((finalTx, ix) => {
|
|
805
|
-
if (!ix) return finalTx // skip null instructions
|
|
806
|
-
|
|
807
|
-
if (!finalTx.token) return ix // init finalTx (support just 1 token type per tx)
|
|
808
|
-
|
|
809
|
-
if (finalTx.token.ticker === ix.token.ticker) finalTx.amount += ix.amount
|
|
810
|
-
return finalTx
|
|
811
|
-
}, {})
|
|
812
|
-
} else if (preTokenBalances && postTokenBalances) {
|
|
813
|
-
// probably a DEX program is involved (multiple instructions), compute balance changes
|
|
814
|
-
const accountIndexes = _lodash.default.mapKeys(accountKeys, (x, i) => i)
|
|
815
|
-
|
|
816
|
-
Object.values(accountIndexes).forEach((acc) => {
|
|
817
|
-
// filter by ownerAddress
|
|
818
|
-
const hasKnownOwner = !!_lodash.default.find(tokenAccountsByOwner, {
|
|
819
|
-
tokenAccountAddress: acc.pubkey,
|
|
820
|
-
})
|
|
821
|
-
acc.owner = hasKnownOwner ? ownerAddress : null
|
|
822
|
-
}) // group by owner and supported token
|
|
823
|
-
|
|
824
|
-
const preBalances = preTokenBalances.filter((t) => {
|
|
825
|
-
return (
|
|
826
|
-
accountIndexes[t.accountIndex].owner === ownerAddress && this.isTokenSupported(t.mint)
|
|
827
|
-
)
|
|
828
|
-
})
|
|
829
|
-
const postBalances = postTokenBalances.filter((t) => {
|
|
830
|
-
return (
|
|
831
|
-
accountIndexes[t.accountIndex].owner === ownerAddress && this.isTokenSupported(t.mint)
|
|
832
|
-
)
|
|
833
|
-
})
|
|
834
|
-
|
|
835
|
-
if (preBalances.length || postBalances.length) {
|
|
836
|
-
tx = {}
|
|
837
|
-
|
|
838
|
-
if (includeUnparsed && innerInstructions.length) {
|
|
839
|
-
// when using includeUnparsed for DEX tx we want to keep SOL tx as "unparsed"
|
|
840
|
-
// 1. we want to treat all SOL dex transactions as "Contract transaction", not "Sent SOL"
|
|
841
|
-
// 2. default behavior is not perfect. For example it doesn't see SOL-side tx in
|
|
842
|
-
// SOL->SPL swaps on Raydium and Orca.
|
|
843
|
-
tx = getUnparsedTx(tx)
|
|
844
|
-
tx.dexTxs = getInnerTxsFromBalanceChanges()
|
|
845
|
-
} else {
|
|
846
|
-
if (solanaTx) {
|
|
847
|
-
// the base tx will be the one that moved solana.
|
|
848
|
-
tx = {
|
|
849
|
-
owner: solanaTx.source,
|
|
850
|
-
from: solanaTx.source,
|
|
851
|
-
to: solanaTx.destination,
|
|
852
|
-
amount: solanaTx.lamports,
|
|
853
|
-
// number
|
|
854
|
-
fee: ownerAddress === solanaTx.source ? fee : 0,
|
|
855
|
-
}
|
|
856
|
-
} // If it has inner instructions then it's a DEX tx that moved SPL -> SPL
|
|
857
|
-
|
|
858
|
-
if (innerInstructions.length) {
|
|
859
|
-
tx.dexTxs = innerInstructions // if tx involves only SPL swaps. Expand DEX ix (first element as tx base and the other kept there)
|
|
860
|
-
|
|
861
|
-
if (!tx.from && !solanaTx) {
|
|
862
|
-
tx = tx.dexTxs[0]
|
|
863
|
-
tx.dexTxs = innerInstructions.slice(1)
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
const unparsed = Object.keys(tx).length === 0
|
|
872
|
-
|
|
873
|
-
if (unparsed && includeUnparsed) {
|
|
874
|
-
tx = getUnparsedTx(tx)
|
|
875
|
-
} // How tokens tx are parsed:
|
|
876
|
-
// 0. compute incoming or outgoing tx: it's outgoing if spl-token:transfer has source/destination included in tokenAccountsByOwner
|
|
877
|
-
// 1. if it's a sent tx: sum all instructions amount (spl-token:transfer)
|
|
878
|
-
// 2. if it's an incoming tx: sum all the amounts with destination included in tokenAccountsByOwner (aggregating by ticker)
|
|
879
|
-
// QUESTION: How do I know what are my tokens addresses deterministically? It's not possible, gotta use tokenAccountsByOwner
|
|
880
|
-
|
|
881
|
-
return {
|
|
882
|
-
id: txDetails.transaction.signatures[0],
|
|
883
|
-
slot: txDetails.slot,
|
|
884
|
-
error: !(txDetails.meta.err === null),
|
|
885
|
-
...tx,
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
async getSupply(mintAddress) {
|
|
890
|
-
const result = await this.rpcCall('getTokenSupply', [mintAddress])
|
|
891
|
-
return _lodash.default.get(result, 'value.amount')
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
async getWalletTokensList({ tokenAccounts }) {
|
|
895
|
-
const tokensMint = []
|
|
896
|
-
|
|
897
|
-
for (let account of tokenAccounts) {
|
|
898
|
-
const mint = account.mintAddress // skip cached NFT
|
|
899
|
-
|
|
900
|
-
if (this.tokensToSkip[mint]) continue // skip 0 balance
|
|
901
|
-
|
|
902
|
-
if (account.balance === '0') continue // skip NFT
|
|
903
|
-
|
|
904
|
-
const supply = await this.getSupply(mint)
|
|
905
|
-
|
|
906
|
-
if (supply === '1') {
|
|
907
|
-
this.tokensToSkip[mint] = true
|
|
908
|
-
continue
|
|
909
|
-
} // OK
|
|
910
|
-
|
|
911
|
-
tokensMint.push(mint)
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
return tokensMint
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
async getTokenAccountsByOwner(address, tokenTicker) {
|
|
918
|
-
const { value: accountsList } = await this.rpcCall(
|
|
919
|
-
'getTokenAccountsByOwner',
|
|
920
|
-
[
|
|
921
|
-
address,
|
|
922
|
-
{
|
|
923
|
-
programId: _solanaLib.TOKEN_PROGRAM_ID.toBase58(),
|
|
924
|
-
},
|
|
925
|
-
{
|
|
926
|
-
encoding: 'jsonParsed',
|
|
927
|
-
},
|
|
928
|
-
],
|
|
929
|
-
{
|
|
930
|
-
address,
|
|
931
|
-
}
|
|
932
|
-
)
|
|
933
|
-
const tokenAccounts = []
|
|
934
|
-
|
|
935
|
-
for (let entry of accountsList) {
|
|
936
|
-
const { pubkey, account } = entry
|
|
937
|
-
|
|
938
|
-
const mint = _lodash.default.get(account, 'data.parsed.info.mint')
|
|
939
|
-
|
|
940
|
-
const token = this.tokens[mint] || {
|
|
941
|
-
name: 'unknown',
|
|
942
|
-
ticker: 'UNKNOWN',
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
const balance = _lodash.default.get(account, 'data.parsed.info.tokenAmount.amount', '0')
|
|
946
|
-
|
|
947
|
-
tokenAccounts.push({
|
|
948
|
-
tokenAccountAddress: pubkey,
|
|
949
|
-
owner: address,
|
|
950
|
-
tokenName: token.name,
|
|
951
|
-
ticker: token.ticker,
|
|
952
|
-
balance,
|
|
953
|
-
mintAddress: mint,
|
|
954
|
-
})
|
|
955
|
-
} // eventually filter by token
|
|
956
|
-
|
|
957
|
-
return tokenTicker
|
|
958
|
-
? tokenAccounts.filter(({ ticker }) => ticker === tokenTicker)
|
|
959
|
-
: tokenAccounts
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
async getTokensBalance({ address, filterByTokens = [], tokenAccounts }) {
|
|
963
|
-
let accounts = tokenAccounts || (await this.getTokenAccountsByOwner(address))
|
|
964
|
-
const tokensBalance = accounts.reduce((acc, { tokenName, balance }) => {
|
|
965
|
-
if (tokenName === 'unknown' || (filterByTokens.length && !filterByTokens.includes(tokenName)))
|
|
966
|
-
return acc // filter by supported tokens only
|
|
967
|
-
|
|
968
|
-
if (!acc[tokenName]) acc[tokenName] = Number(balance)
|
|
969
|
-
// e.g { 'serum': 123 }
|
|
970
|
-
else acc[tokenName] += Number(balance) // merge same token account balance
|
|
971
|
-
|
|
972
|
-
return acc
|
|
973
|
-
}, {})
|
|
974
|
-
return tokensBalance
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
async isAssociatedTokenAccountActive(tokenAddress) {
|
|
978
|
-
// Returns the token balance of an SPL Token account.
|
|
979
|
-
try {
|
|
980
|
-
await this.rpcCall('getTokenAccountBalance', [tokenAddress])
|
|
981
|
-
return true
|
|
982
|
-
} catch (e) {
|
|
983
|
-
return false
|
|
984
|
-
}
|
|
985
|
-
} // Returns account balance of a SPL Token account.
|
|
986
|
-
|
|
987
|
-
async getTokenBalance(tokenAddress) {
|
|
988
|
-
const result = await this.rpcCall('getTokenAccountBalance', [tokenAddress])
|
|
989
|
-
return _lodash.default.get(result, 'value.amount')
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
async getAccountInfo(address, encoding = 'jsonParsed') {
|
|
993
|
-
const { value } = await this.rpcCall(
|
|
994
|
-
'getAccountInfo',
|
|
995
|
-
[
|
|
996
|
-
address,
|
|
997
|
-
{
|
|
998
|
-
encoding,
|
|
999
|
-
commitment: 'single',
|
|
1000
|
-
},
|
|
1001
|
-
],
|
|
1002
|
-
{
|
|
1003
|
-
address,
|
|
1004
|
-
}
|
|
1005
|
-
)
|
|
1006
|
-
return value
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
async isSpl(address) {
|
|
1010
|
-
const { owner } = await this.getAccountInfo(address)
|
|
1011
|
-
return owner === 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
async getMetaplexMetadata(tokenMintAddress) {
|
|
1015
|
-
const metaplexPDA = (0, _solanaLib.getMetadataAccount)(tokenMintAddress)
|
|
1016
|
-
const res = await this.getAccountInfo(metaplexPDA, 'base64')
|
|
1017
|
-
|
|
1018
|
-
const data = _lodash.default.get(res, 'data[0]')
|
|
1019
|
-
|
|
1020
|
-
if (!data) return null
|
|
1021
|
-
return (0, _solanaLib.deserializeMetaplexMetadata)(Buffer.from(data, 'base64'))
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
async getDecimals(tokenMintAddress) {
|
|
1025
|
-
const result = await this.rpcCall('getTokenSupply', [tokenMintAddress])
|
|
1026
|
-
return _lodash.default.get(result, 'value.decimals', null)
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
async getAddressType(address) {
|
|
1030
|
-
// solana, token or null (unknown), meaning address has never been initialized
|
|
1031
|
-
const value = await this.getAccountInfo(address)
|
|
1032
|
-
if (value === null) return null
|
|
1033
|
-
const account = {
|
|
1034
|
-
executable: value.executable,
|
|
1035
|
-
owner: value.owner,
|
|
1036
|
-
lamports: value.lamports,
|
|
1037
|
-
}
|
|
1038
|
-
return account.owner === _solanaLib.SYSTEM_PROGRAM_ID.toBase58()
|
|
1039
|
-
? 'solana'
|
|
1040
|
-
: account.owner === _solanaLib.TOKEN_PROGRAM_ID.toBase58()
|
|
1041
|
-
? 'token'
|
|
1042
|
-
: null
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1045
|
-
async getTokenAddressOwner(address) {
|
|
1046
|
-
const value = await this.getAccountInfo(address)
|
|
1047
|
-
|
|
1048
|
-
const owner = _lodash.default.get(value, 'data.parsed.info.owner', null)
|
|
1049
|
-
|
|
1050
|
-
return owner
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
|
-
async getAddressMint(address) {
|
|
1054
|
-
const value = await this.getAccountInfo(address)
|
|
1055
|
-
|
|
1056
|
-
const mintAddress = _lodash.default.get(value, 'data.parsed.info.mint', null) // token mint
|
|
1057
|
-
|
|
1058
|
-
return mintAddress
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
async isTokenAddress(address) {
|
|
1062
|
-
const type = await this.getAddressType(address)
|
|
1063
|
-
return type === 'token'
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
async isSOLaddress(address) {
|
|
1067
|
-
const type = await this.getAddressType(address)
|
|
1068
|
-
return type === 'solana'
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
|
-
async getStakeAccountsInfo(address) {
|
|
1072
|
-
const params = [
|
|
1073
|
-
_solanaLib.STAKE_PROGRAM_ID.toBase58(),
|
|
1074
|
-
{
|
|
1075
|
-
filters: [
|
|
1076
|
-
{
|
|
1077
|
-
memcmp: {
|
|
1078
|
-
offset: 12,
|
|
1079
|
-
bytes: address,
|
|
1080
|
-
},
|
|
1081
|
-
},
|
|
1082
|
-
],
|
|
1083
|
-
encoding: 'jsonParsed',
|
|
1084
|
-
},
|
|
1085
|
-
]
|
|
1086
|
-
const res = await this.rpcCall('getProgramAccounts', params, {
|
|
1087
|
-
address,
|
|
1088
|
-
})
|
|
1089
|
-
const accounts = {}
|
|
1090
|
-
let totalStake = 0
|
|
1091
|
-
let locked = 0
|
|
1092
|
-
let withdrawable = 0
|
|
1093
|
-
let pending = 0
|
|
1094
|
-
|
|
1095
|
-
for (let entry of res) {
|
|
1096
|
-
const addr = entry.pubkey
|
|
1097
|
-
|
|
1098
|
-
const lamports = _lodash.default.get(entry, 'account.lamports', 0)
|
|
1099
|
-
|
|
1100
|
-
const delegation = _lodash.default.get(entry, 'account.data.parsed.info.stake.delegation', {}) // could have no delegation if the created stake address did not perform a delegate transaction
|
|
1101
|
-
|
|
1102
|
-
accounts[addr] = delegation
|
|
1103
|
-
accounts[addr].lamports = lamports // sol balance
|
|
1104
|
-
|
|
1105
|
-
accounts[addr].activationEpoch = Number(accounts[addr].activationEpoch) || 0
|
|
1106
|
-
accounts[addr].deactivationEpoch = Number(accounts[addr].deactivationEpoch) || 0
|
|
1107
|
-
let state = 'inactive'
|
|
1108
|
-
if (delegation.activationEpoch) state = await this.getStakeActivation(addr)
|
|
1109
|
-
accounts[addr].state = state
|
|
1110
|
-
accounts[addr].isDeactivating = state === 'deactivating'
|
|
1111
|
-
accounts[addr].canWithdraw = state === 'inactive'
|
|
1112
|
-
accounts[addr].stake = Number(accounts[addr].stake) || 0 // active staked amount
|
|
1113
|
-
|
|
1114
|
-
totalStake += accounts[addr].stake
|
|
1115
|
-
locked += ['active', 'activating'].includes(accounts[addr].state) ? lamports : 0
|
|
1116
|
-
withdrawable += accounts[addr].canWithdraw ? lamports : 0
|
|
1117
|
-
pending += accounts[addr].isDeactivating ? lamports : 0
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
return {
|
|
1121
|
-
accounts,
|
|
1122
|
-
totalStake,
|
|
1123
|
-
locked,
|
|
1124
|
-
withdrawable,
|
|
1125
|
-
pending,
|
|
1126
|
-
}
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
async getRewards(stakingAddresses = []) {
|
|
1130
|
-
if (!stakingAddresses.length) return 0 // custom endpoint!
|
|
1131
|
-
|
|
1132
|
-
const rewards = await this.request(`rewards?addresses=${stakingAddresses.join(',')}`)
|
|
1133
|
-
.get()
|
|
1134
|
-
.error(500, () => ({})) // addresses not found
|
|
1135
|
-
.error(400, () => ({}))
|
|
1136
|
-
.json() // sum rewards for all addresses
|
|
1137
|
-
|
|
1138
|
-
const earnings = Object.values(rewards).reduce((total, x) => {
|
|
1139
|
-
return total + x
|
|
1140
|
-
}, 0)
|
|
1141
|
-
return earnings
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
async getMinimumBalanceForRentExemption(size) {
|
|
1145
|
-
return this.rpcCall('getMinimumBalanceForRentExemption', [size])
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
|
-
async getProgramAccounts(programId, config) {
|
|
1149
|
-
return this.rpcCall('getProgramAccounts', [programId, config])
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
|
-
async getMultipleAccounts(pubkeys, config) {
|
|
1153
|
-
const response = await this.rpcCall('getMultipleAccounts', [pubkeys, config])
|
|
1154
|
-
return response && response.value ? response.value : []
|
|
1155
|
-
}
|
|
1156
|
-
/**
|
|
1157
|
-
* Broadcast a signed transaction
|
|
1158
|
-
*/
|
|
1159
|
-
}
|
|
1160
|
-
|
|
1161
|
-
exports.Api = Api
|