@exodus/solana-api 1.2.14 → 1.2.16

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.
Files changed (3) hide show
  1. package/package.json +7 -5
  2. package/src/index.js +465 -0
  3. package/lib/index.js +0 -512
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@exodus/solana-api",
3
- "version": "1.2.14",
3
+ "version": "1.2.16",
4
4
  "description": "Exodus internal Solana asset API wrapper",
5
- "main": "lib/index.js",
5
+ "main": "src/index.js",
6
6
  "files": [
7
- "lib/",
7
+ "src/",
8
8
  "!src/__tests__"
9
9
  ],
10
10
  "author": "Exodus",
@@ -14,11 +14,13 @@
14
14
  },
15
15
  "dependencies": {
16
16
  "@exodus/asset-json-rpc": "^1.0.0",
17
- "@exodus/solana-lib": "^1.2.14",
17
+ "@exodus/solana-lib": "^1.2.16",
18
18
  "lodash": "^4.17.11",
19
+ "url-join": "4.0.0",
19
20
  "wretch": "^1.5.2"
20
21
  },
21
22
  "devDependencies": {
22
23
  "node-fetch": "~1.6.3"
23
- }
24
+ },
25
+ "gitHead": "882b8a97ef05cbd0fbd7da0b2ce457e0a0ae7e35"
24
26
  }
package/src/index.js ADDED
@@ -0,0 +1,465 @@
1
+ // @flow
2
+ import createApi from '@exodus/asset-json-rpc'
3
+ import { tokens, SYSTEM_PROGRAM_ID, STAKE_PROGRAM_ID, TOKEN_PROGRAM_ID } from '@exodus/solana-lib'
4
+ import assert from 'assert'
5
+ import lodash from 'lodash'
6
+ import urljoin from 'url-join'
7
+ import wretch, { Wretcher } from 'wretch'
8
+
9
+ // Doc: https://docs.solana.com/apps/jsonrpc-api
10
+
11
+ 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
12
+
13
+ // Tokens + SOL api support
14
+ class Api {
15
+ constructor(rpcUrl) {
16
+ this.setServer(rpcUrl)
17
+ }
18
+
19
+ setServer(rpcUrl) {
20
+ this.rpcUrl = rpcUrl || RPC_URL
21
+ this.api = createApi(this.rpcUrl)
22
+ }
23
+
24
+ request(path, contentType = 'application/json'): Wretcher {
25
+ return wretch(urljoin(this.rpcUrl, path)).headers({
26
+ 'Content-type': contentType,
27
+ })
28
+ }
29
+
30
+ async getEpochInfo(): number {
31
+ const { epoch } = await this.api.post({
32
+ method: 'getEpochInfo',
33
+ })
34
+ return Number(epoch)
35
+ }
36
+
37
+ async getStakeActivation(address): string {
38
+ const { state } = await this.api.post({
39
+ method: 'getStakeActivation',
40
+ params: [address],
41
+ })
42
+ return state
43
+ }
44
+
45
+ async getRecentBlockHash(): string {
46
+ const {
47
+ value: { blockhash },
48
+ } = await this.api.post({
49
+ method: 'getRecentBlockhash',
50
+ })
51
+ return blockhash
52
+ }
53
+
54
+ // Transaction structure: https://docs.solana.com/apps/jsonrpc-api#transaction-structure
55
+ async getTransactionById(id: string) {
56
+ const result = await this.api.post({
57
+ method: 'getConfirmedTransaction',
58
+ params: [id, 'jsonParsed'],
59
+ })
60
+ return result
61
+ }
62
+
63
+ async getFee(): number {
64
+ const {
65
+ value: {
66
+ feeCalculator: { lamportsPerSignature },
67
+ },
68
+ } = await this.api.post({
69
+ method: 'getRecentBlockhash',
70
+ })
71
+ return lamportsPerSignature
72
+ }
73
+
74
+ async getBalance(address: string): number {
75
+ const res = await this.api.post({
76
+ method: 'getBalance',
77
+ params: [address],
78
+ })
79
+ return res.value || 0
80
+ }
81
+
82
+ async getBlockTime(slot: number) {
83
+ // might result in error if executed on a validator with partial ledger (https://github.com/solana-labs/solana/issues/12413)
84
+ return this.api.post({
85
+ method: 'getBlockTime',
86
+ params: [slot],
87
+ })
88
+ }
89
+
90
+ async getConfirmedSignaturesForAddress(address: string, { until, before, limit } = {}): any {
91
+ until = until || undefined
92
+ return this.api.post({
93
+ method: 'getConfirmedSignaturesForAddress2',
94
+ params: [address, { until, before, limit }],
95
+ })
96
+ }
97
+
98
+ /**
99
+ * Get transactions from an address
100
+ */
101
+ async getTransactions(address: string, { cursor, before, limit } = {}): any {
102
+ let transactions = []
103
+ // cursor is a txHash
104
+
105
+ try {
106
+ let until = cursor
107
+
108
+ const tokenAccountsByOwner = await this.getTokenAccountsByOwner(address) // Array
109
+ const tokenAccountAddresses = tokenAccountsByOwner
110
+ .filter(({ tokenName }) => tokenName !== 'unknown')
111
+ .map(({ tokenAccountAddress }) => tokenAccountAddress)
112
+ const accountsToCheck = [address, ...tokenAccountAddresses]
113
+
114
+ const txsResultsByAccount = await Promise.all(
115
+ accountsToCheck.map((addr) =>
116
+ this.getConfirmedSignaturesForAddress(addr, {
117
+ until,
118
+ before,
119
+ limit,
120
+ })
121
+ )
122
+ )
123
+ let txsId = txsResultsByAccount.reduce((arr, row) => arr.concat(row), []) // merge arrays
124
+ txsId = lodash.uniqBy(txsId, 'signature')
125
+
126
+ // get txs details in parallel
127
+ const txsDetails = await Promise.all(txsId.map((tx) => this.getTransactionById(tx.signature)))
128
+ txsDetails.forEach((txDetail) => {
129
+ if (txDetail === null) return
130
+
131
+ const timestamp = txDetail.blockTime * 1000
132
+ const parsedTx = Api.parseTransaction(address, txDetail, tokenAccountsByOwner)
133
+ if (!parsedTx.from) return // cannot parse it
134
+
135
+ transactions.push({
136
+ timestamp,
137
+ date: new Date(timestamp),
138
+ ...parsedTx,
139
+ })
140
+ })
141
+ } catch (err) {
142
+ console.warn('Solana error:', err)
143
+ throw err
144
+ }
145
+
146
+ transactions = lodash.orderBy(transactions, ['timestamp'], ['desc'])
147
+
148
+ const newCursor = transactions[0] ? transactions[0].id : cursor
149
+
150
+ return { transactions, newCursor }
151
+ }
152
+
153
+ parseTransaction(...args) {
154
+ // alias
155
+ return Api.parseTransaction(...args)
156
+ }
157
+
158
+ static parseTransaction(
159
+ ownerAddress: string,
160
+ txDetails: Object,
161
+ tokenAccountsByOwner: ?Array
162
+ ): Object {
163
+ const { fee } = txDetails.meta
164
+ let { instructions } = txDetails.transaction.message
165
+ instructions = instructions
166
+ .filter((ix) => ix.parsed) // only known instructions
167
+ .map((ix) => ({
168
+ program: ix.program, // system or spl-token
169
+ type: ix.parsed.type, // transfer, createAccount, initializeAccount
170
+ ...ix.parsed.info,
171
+ }))
172
+
173
+ // program:type tells us if it's a SOL or Token transfer
174
+ const solanaTx = lodash.find(instructions, { program: 'system', type: 'transfer' }) // get SOL transfer
175
+ const stakeTx = lodash.find(instructions, { program: 'system', type: 'createAccountWithSeed' })
176
+ const stakeWithdraw = lodash.find(instructions, { program: 'stake', type: 'withdraw' })
177
+ const stakeUndelegate = lodash.find(instructions, { program: 'stake', type: 'deactivate' })
178
+
179
+ let tx
180
+ if (solanaTx) {
181
+ // Solana tx
182
+ const isSending = ownerAddress === solanaTx.source
183
+ tx = {
184
+ owner: solanaTx.source,
185
+ from: solanaTx.source,
186
+ to: solanaTx.destination,
187
+ amount: solanaTx.lamports, // number
188
+ fee: isSending ? fee : 0,
189
+ }
190
+ } else if (stakeTx) {
191
+ // start staking
192
+ tx = {
193
+ owner: stakeTx.base,
194
+ from: stakeTx.base,
195
+ to: stakeTx.base,
196
+ amount: stakeTx.lamports,
197
+ fee,
198
+ staking: {
199
+ method: 'createAccountWithSeed',
200
+ seed: stakeTx.seed,
201
+ stakeAddress: stakeTx.newAccount,
202
+ stake: stakeTx.lamports,
203
+ },
204
+ }
205
+ } else if (stakeWithdraw) {
206
+ // TODO: lodash.find above returns 1 occurence, there could be multiple withdraw instructions in the same tx.
207
+ tx = {
208
+ owner: stakeWithdraw.withdrawAuthority,
209
+ from: stakeWithdraw.stakeAccount,
210
+ to: stakeWithdraw.destination,
211
+ amount: stakeWithdraw.lamports,
212
+ fee,
213
+ staking: {
214
+ method: 'withdraw',
215
+ stakeAddress: stakeWithdraw.stakeAccount,
216
+ stake: stakeWithdraw.lamports,
217
+ },
218
+ }
219
+ } else if (stakeUndelegate) {
220
+ tx = {
221
+ owner: stakeUndelegate.stakeAuthority,
222
+ from: stakeUndelegate.stakeAuthority,
223
+ to: stakeUndelegate.stakeAccount,
224
+ amount: 0,
225
+ fee,
226
+ staking: {
227
+ method: 'undelegate',
228
+ stakeAddress: stakeUndelegate.stakeAccount,
229
+ },
230
+ }
231
+ } else {
232
+ // Token tx
233
+ assert.ok(
234
+ Array.isArray(tokenAccountsByOwner),
235
+ 'tokenAccountsByOwner is required when parsing token tx'
236
+ )
237
+ let tokenTxs = lodash
238
+ .filter(instructions, { program: 'spl-token', type: 'transfer' }) // get Token transfer: could have more than 1 instructions
239
+ .map((ix) => {
240
+ // add token details based on source/destination address
241
+ let tokenAccount = lodash.find(tokenAccountsByOwner, { tokenAccountAddress: ix.source })
242
+ const isSending = !!tokenAccount
243
+ if (!isSending)
244
+ tokenAccount = lodash.find(tokenAccountsByOwner, {
245
+ tokenAccountAddress: ix.destination,
246
+ }) // receiving
247
+ if (!tokenAccount) return null // no transfers with our addresses involved
248
+ const owner = isSending ? ownerAddress : null
249
+
250
+ delete tokenAccount.balance
251
+ delete tokenAccount.owner
252
+ return {
253
+ owner,
254
+ token: tokenAccount,
255
+ from: ix.source,
256
+ to: ix.destination,
257
+ amount: Number(ix.amount), // token
258
+ fee: isSending ? fee : 0, // in lamports
259
+ }
260
+ })
261
+
262
+ // .reduce to sum/sub (based on isSending) all the same tokens amount (From instructions -> 1 single tx)
263
+ tx = tokenTxs.reduce((finalTx, ix) => {
264
+ if (!ix) return finalTx // skip null instructions
265
+ if (!finalTx.token) return ix // init finalTx (support just 1 token type per tx)
266
+ if (finalTx.token.ticker === ix.token.ticker) finalTx.amount += ix.amount
267
+ return finalTx
268
+ }, {})
269
+ }
270
+
271
+ // How tokens tx are parsed:
272
+ // 0. compute incoming or outgoing tx: it's outgoing if spl-token:transfer has source/destination included in tokenAccountsByOwner
273
+ // 1. if it's a sent tx: sum all instructions amount (spl-token:transfer)
274
+ // 2. if it's an incoming tx: sull all the amounts with destination included in tokenAccountsByOwner (aggregating by ticker)
275
+ // QUESTION: How do I know what are my tokens addresses deterministically? It's not possible, gotta use tokenAccountsByOwner
276
+
277
+ return {
278
+ id: txDetails.transaction.signatures[0],
279
+ slot: txDetails.slot,
280
+ error: !(txDetails.meta.err === null),
281
+ ...tx,
282
+ }
283
+ }
284
+
285
+ async getTokenAccountsByOwner(address: string, tokenTicker: ?string): Array {
286
+ const { value: accountsList } = await this.api.post({
287
+ method: 'getTokenAccountsByOwner',
288
+ params: [address, { programId: TOKEN_PROGRAM_ID.toBase58() }, { encoding: 'jsonParsed' }],
289
+ })
290
+
291
+ const tokenAccounts = []
292
+ for (let entry of accountsList) {
293
+ const { pubkey, account } = entry
294
+
295
+ const mint = lodash.get(account, 'data.parsed.info.mint')
296
+ const token = tokens.find(({ mintAddress }) => mintAddress === mint) || {
297
+ tokenName: 'unknown',
298
+ ticker: 'UNKNOWN',
299
+ }
300
+ const balance = lodash.get(account, 'data.parsed.info.tokenAmount.amount', '0')
301
+ tokenAccounts.push({
302
+ tokenAccountAddress: pubkey,
303
+ owner: address,
304
+ tokenName: token.tokenName,
305
+ ticker: token.ticker,
306
+ balance,
307
+ })
308
+ }
309
+ // eventually filter by token
310
+ return tokenTicker
311
+ ? tokenAccounts.filter(({ ticker }) => ticker === tokenTicker)
312
+ : tokenAccounts
313
+ }
314
+
315
+ async getTokensBalance(address: string, filterByTokens = []) {
316
+ let accounts = await this.getTokenAccountsByOwner(address) // Tokens
317
+
318
+ const tokensBalance = accounts.reduce((acc, { tokenName, balance }) => {
319
+ if (tokenName === 'unknown' || (filterByTokens.length && !filterByTokens.includes(tokenName)))
320
+ return acc // filter by supported tokens only
321
+ if (!acc[tokenName]) acc[tokenName] = Number(balance)
322
+ // e.g { 'serum': 123 }
323
+ else acc[tokenName] += Number(balance) // merge same token account balance
324
+ return acc
325
+ }, {})
326
+
327
+ return tokensBalance
328
+ }
329
+
330
+ async isAssociatedTokenAccountActive(tokenAddress: string) {
331
+ // Returns the token balance of an SPL Token account.
332
+ try {
333
+ await this.api.post({
334
+ method: 'getTokenAccountBalance',
335
+ params: [tokenAddress],
336
+ })
337
+ return true
338
+ } catch (e) {
339
+ return false
340
+ }
341
+ }
342
+
343
+ async getAddressType(address: string) {
344
+ // solana, token or null (unknown), meaning address has never been initialized
345
+ const { value } = await this.api.post({
346
+ method: 'getAccountInfo',
347
+ params: [address, { encoding: 'base64' }],
348
+ })
349
+ if (value === null) return null
350
+
351
+ const account = {
352
+ executable: value.executable,
353
+ owner: value.owner,
354
+ lamports: value.lamports,
355
+ data: value.data,
356
+ }
357
+
358
+ return account.owner === SYSTEM_PROGRAM_ID.toBase58()
359
+ ? 'solana'
360
+ : account.owner === TOKEN_PROGRAM_ID.toBase58()
361
+ ? 'token'
362
+ : null
363
+ }
364
+
365
+ async isTokenAddress(address: string) {
366
+ const type = await this.getAddressType(address)
367
+ return type === 'token'
368
+ }
369
+
370
+ async isSOLaddress(address: string) {
371
+ const type = await this.getAddressType(address)
372
+ return type === 'solana'
373
+ }
374
+
375
+ async getStakeAccountsInfo(address: string) {
376
+ // get staked amount and other info
377
+ const res = await this.api.post({
378
+ method: 'getProgramAccounts',
379
+ params: [
380
+ STAKE_PROGRAM_ID.toBase58(),
381
+ {
382
+ filters: [
383
+ {
384
+ memcmp: {
385
+ offset: 12,
386
+ bytes: address,
387
+ },
388
+ },
389
+ ],
390
+ encoding: 'jsonParsed',
391
+ },
392
+ ],
393
+ })
394
+ const accounts = {}
395
+ let totalStake = 0
396
+ let locked = 0
397
+ let withdrawable = 0
398
+ let pending = 0
399
+ for (let entry of res) {
400
+ const addr = entry.pubkey
401
+ const lamports = lodash.get(entry, 'account.lamports', 0)
402
+ const delegation = lodash.get(entry, 'account.data.parsed.info.stake.delegation', {})
403
+ // could have no delegation if the created stake address did not perform a delegate transaction
404
+
405
+ accounts[addr] = delegation
406
+ accounts[addr].lamports = lamports // sol balance
407
+ accounts[addr].activationEpoch = Number(accounts[addr].activationEpoch) || 0
408
+ accounts[addr].deactivationEpoch = Number(accounts[addr].deactivationEpoch) || 0
409
+ let state = 'inactive'
410
+ if (delegation.activationEpoch) state = await this.getStakeActivation(addr)
411
+ accounts[addr].state = state
412
+ accounts[addr].isDeactivating = state === 'deactivating'
413
+ accounts[addr].canWithdraw = state === 'inactive'
414
+ accounts[addr].stake = Number(accounts[addr].stake) || 0 // active staked amount
415
+ totalStake += accounts[addr].stake
416
+ locked += ['active', 'activating'].includes(accounts[addr].state) ? lamports : 0
417
+ withdrawable += accounts[addr].canWithdraw ? lamports : 0
418
+ pending += accounts[addr].isDeactivating ? lamports : 0
419
+ }
420
+ return { accounts, totalStake, locked, withdrawable, pending }
421
+ }
422
+
423
+ async getInflationReward(stakingAddresses: Array, epoch: number) {
424
+ const rewards = await this.api.post({
425
+ method: 'getInflationReward',
426
+ params: [stakingAddresses, { epoch }],
427
+ })
428
+
429
+ return rewards
430
+ }
431
+
432
+ async getRewards(stakingAddresses = []) {
433
+ if (!stakingAddresses.length) return 0
434
+
435
+ const rewards = await this.request(`rewards?addresses=${stakingAddresses.join(',')}`)
436
+ .get()
437
+ .error(500, () => ({})) // addresses not found
438
+ .error(400, () => ({}))
439
+ .json()
440
+
441
+ // sum rewards for all addresses
442
+ const earnings = Object.values(rewards).reduce((total, x) => {
443
+ return total + x
444
+ }, 0)
445
+
446
+ return earnings
447
+ }
448
+
449
+ /**
450
+ * Broadcast a signed transaction
451
+ */
452
+ broadcastTransaction = async (signedTx: string): string => {
453
+ console.log('Solana broadcasting TX:', signedTx) // base64
454
+
455
+ const result = await this.api.post({
456
+ method: 'sendTransaction',
457
+ params: [signedTx, { encoding: 'base64', commitment: 'singleGossip' }],
458
+ })
459
+
460
+ console.log(`tx ${JSON.stringify(result)} sent!`)
461
+ return result || null
462
+ }
463
+ }
464
+
465
+ export default new Api()
package/lib/index.js DELETED
@@ -1,512 +0,0 @@
1
- "use strict";
2
-
3
- Object.defineProperty(exports, "__esModule", {
4
- value: true
5
- });
6
- exports.default = void 0;
7
-
8
- var _assetJsonRpc = _interopRequireDefault(require("@exodus/asset-json-rpc"));
9
-
10
- var _solanaLib = require("@exodus/solana-lib");
11
-
12
- var _assert = _interopRequireDefault(require("assert"));
13
-
14
- var _lodash = _interopRequireDefault(require("lodash"));
15
-
16
- function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
17
-
18
- function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; }
19
-
20
- function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }
21
-
22
- function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
23
-
24
- // Doc: https://docs.solana.com/apps/jsonrpc-api
25
- 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
26
- // Tokens + SOL api support
27
-
28
- class Api {
29
- constructor(rpcUrl) {
30
- _defineProperty(this, "broadcastTransaction", async signedTx => {
31
- console.log('Solana broadcasting TX:', signedTx); // base64
32
-
33
- const result = await this.api.post({
34
- method: 'sendTransaction',
35
- params: [signedTx, {
36
- encoding: 'base64',
37
- commitment: 'singleGossip'
38
- }]
39
- });
40
- console.log(`tx ${JSON.stringify(result)} sent!`);
41
- return result || null;
42
- });
43
-
44
- this.setServer(rpcUrl);
45
- }
46
-
47
- setServer(rpcUrl) {
48
- this.rpcUrl = rpcUrl || RPC_URL;
49
- this.api = (0, _assetJsonRpc.default)(this.rpcUrl);
50
- }
51
-
52
- async getCurrentEpoch() {
53
- const {
54
- epoch
55
- } = await this.api.post({
56
- method: 'getEpochInfo'
57
- });
58
- return Number(epoch);
59
- }
60
-
61
- async getStakeActivation(address) {
62
- const {
63
- state
64
- } = await this.api.post({
65
- method: 'getStakeActivation',
66
- params: [address]
67
- });
68
- return state;
69
- }
70
-
71
- async getRecentBlockHash() {
72
- const {
73
- value: {
74
- blockhash
75
- }
76
- } = await this.api.post({
77
- method: 'getRecentBlockhash'
78
- });
79
- return blockhash;
80
- } // Transaction structure: https://docs.solana.com/apps/jsonrpc-api#transaction-structure
81
-
82
-
83
- async getTransactionById(id) {
84
- const result = await this.api.post({
85
- method: 'getConfirmedTransaction',
86
- params: [id, 'jsonParsed']
87
- });
88
- return result;
89
- }
90
-
91
- async getFee() {
92
- const {
93
- value: {
94
- feeCalculator: {
95
- lamportsPerSignature
96
- }
97
- }
98
- } = await this.api.post({
99
- method: 'getRecentBlockhash'
100
- });
101
- return lamportsPerSignature;
102
- }
103
-
104
- async getBalance(address) {
105
- const res = await this.api.post({
106
- method: 'getBalance',
107
- params: [address]
108
- });
109
- return res.value || 0;
110
- }
111
-
112
- async getBlockTime(slot) {
113
- // might result in error if executed on a validator with partial ledger (https://github.com/solana-labs/solana/issues/12413)
114
- return this.api.post({
115
- method: 'getBlockTime',
116
- params: [slot]
117
- });
118
- }
119
-
120
- async getConfirmedSignaturesForAddress(address, {
121
- until,
122
- before,
123
- limit
124
- } = {}) {
125
- until = until || undefined;
126
- return this.api.post({
127
- method: 'getConfirmedSignaturesForAddress2',
128
- params: [address, {
129
- until,
130
- before,
131
- limit
132
- }]
133
- });
134
- }
135
- /**
136
- * Get transactions from an address
137
- */
138
-
139
-
140
- async getTransactions(address, {
141
- cursor,
142
- before,
143
- limit
144
- } = {}) {
145
- let transactions = []; // cursor is a txHash
146
-
147
- try {
148
- let until = cursor;
149
- const tokenAccountsByOwner = await this.getTokenAccountsByOwner(address); // Array
150
-
151
- const tokenAccountAddresses = tokenAccountsByOwner.filter(({
152
- tokenName
153
- }) => tokenName !== 'unknown').map(({
154
- tokenAccountAddress
155
- }) => tokenAccountAddress);
156
- const accountsToCheck = [address, ...tokenAccountAddresses];
157
- const txsResultsByAccount = await Promise.all(accountsToCheck.map(addr => this.getConfirmedSignaturesForAddress(addr, {
158
- until,
159
- before,
160
- limit
161
- })));
162
- let txsId = txsResultsByAccount.reduce((arr, row) => arr.concat(row), []); // merge arrays
163
-
164
- txsId = _lodash.default.uniqBy(txsId, 'signature'); // get txs details in parallel
165
-
166
- const txsDetails = await Promise.all(txsId.map(tx => this.getTransactionById(tx.signature)));
167
- txsDetails.forEach(txDetail => {
168
- if (txDetail === null) return;
169
- const timestamp = txDetail.blockTime * 1000;
170
- const parsedTx = Api.parseTransaction(address, txDetail, tokenAccountsByOwner);
171
- if (!parsedTx.from) return; // cannot parse it
172
-
173
- transactions.push(_objectSpread({
174
- timestamp,
175
- date: new Date(timestamp)
176
- }, parsedTx));
177
- });
178
- } catch (err) {
179
- console.warn('Solana error:', err);
180
- throw err;
181
- }
182
-
183
- transactions = _lodash.default.orderBy(transactions, ['timestamp'], ['desc']);
184
- const newCursor = transactions[0] ? transactions[0].id : cursor;
185
- return {
186
- transactions,
187
- newCursor
188
- };
189
- }
190
-
191
- parseTransaction(...args) {
192
- // alias
193
- return Api.parseTransaction(...args);
194
- }
195
-
196
- static parseTransaction(ownerAddress, txDetails, tokenAccountsByOwner) {
197
- const {
198
- fee
199
- } = txDetails.meta;
200
- let {
201
- instructions
202
- } = txDetails.transaction.message;
203
- instructions = instructions.filter(ix => ix.parsed) // only known instructions
204
- .map(ix => _objectSpread({
205
- program: ix.program,
206
- // system or spl-token
207
- type: ix.parsed.type
208
- }, ix.parsed.info)); // program:type tells us if it's a SOL or Token transfer
209
-
210
- const solanaTx = _lodash.default.find(instructions, {
211
- program: 'system',
212
- type: 'transfer'
213
- }); // get SOL transfer
214
-
215
-
216
- const stakeTx = _lodash.default.find(instructions, {
217
- program: 'system',
218
- type: 'createAccountWithSeed'
219
- });
220
-
221
- const stakeWithdraw = _lodash.default.find(instructions, {
222
- program: 'stake',
223
- type: 'withdraw'
224
- });
225
-
226
- const stakeUndelegate = _lodash.default.find(instructions, {
227
- program: 'stake',
228
- type: 'deactivate'
229
- });
230
-
231
- let tx;
232
-
233
- if (solanaTx) {
234
- // Solana tx
235
- const isSending = ownerAddress === solanaTx.source;
236
- tx = {
237
- owner: solanaTx.source,
238
- from: solanaTx.source,
239
- to: solanaTx.destination,
240
- amount: solanaTx.lamports,
241
- // number
242
- fee: isSending ? fee : 0
243
- };
244
- } else if (stakeTx) {
245
- // start staking
246
- tx = {
247
- owner: stakeTx.base,
248
- from: stakeTx.base,
249
- to: stakeTx.owner,
250
- amount: stakeTx.lamports,
251
- fee,
252
- staking: {
253
- method: 'createAccountWithSeed',
254
- seed: stakeTx.seed,
255
- stakeAddress: stakeTx.newAccount,
256
- stake: stakeTx.lamports
257
- }
258
- };
259
- } else if (stakeWithdraw) {
260
- // TODO: lodash.find above returns 1 occurence, there could be multiple withdraw instructions in the same tx.
261
- tx = {
262
- owner: stakeWithdraw.withdrawAuthority,
263
- from: stakeWithdraw.stakeAccount,
264
- to: stakeWithdraw.destination,
265
- amount: stakeWithdraw.lamports,
266
- fee,
267
- staking: {
268
- method: 'withdraw',
269
- stakeAddress: stakeWithdraw.stakeAccount,
270
- stake: stakeWithdraw.lamports
271
- }
272
- };
273
- } else if (stakeUndelegate) {
274
- tx = {
275
- owner: stakeUndelegate.stakeAuthority,
276
- from: stakeUndelegate.stakeAuthority,
277
- to: stakeUndelegate.stakeAccount,
278
- amount: 0,
279
- fee,
280
- staking: {
281
- method: 'undelegate',
282
- stakeAddress: stakeUndelegate.stakeAccount
283
- }
284
- };
285
- } else {
286
- // Token tx
287
- _assert.default.ok(Array.isArray(tokenAccountsByOwner), 'tokenAccountsByOwner is required when parsing token tx');
288
-
289
- let tokenTxs = _lodash.default.filter(instructions, {
290
- program: 'spl-token',
291
- type: 'transfer'
292
- }) // get Token transfer: could have more than 1 instructions
293
- .map(ix => {
294
- // add token details based on source/destination address
295
- let tokenAccount = _lodash.default.find(tokenAccountsByOwner, {
296
- tokenAccountAddress: ix.source
297
- });
298
-
299
- const isSending = !!tokenAccount;
300
- if (!isSending) tokenAccount = _lodash.default.find(tokenAccountsByOwner, {
301
- tokenAccountAddress: ix.destination
302
- }); // receiving
303
-
304
- if (!tokenAccount) return null; // no transfers with our addresses involved
305
-
306
- const owner = isSending ? ownerAddress : null;
307
- delete tokenAccount.balance;
308
- delete tokenAccount.owner;
309
- return {
310
- owner,
311
- token: tokenAccount,
312
- from: ix.source,
313
- to: ix.destination,
314
- amount: Number(ix.amount),
315
- // token
316
- fee: isSending ? fee : 0 // in lamports
317
-
318
- };
319
- }); // .reduce to sum/sub (based on isSending) all the same tokens amount (From instructions -> 1 single tx)
320
-
321
-
322
- tx = tokenTxs.reduce((finalTx, ix) => {
323
- if (!ix) return finalTx; // skip null instructions
324
-
325
- if (!finalTx.token) return ix; // init finalTx (support just 1 token type per tx)
326
-
327
- if (finalTx.token.ticker === ix.token.ticker) finalTx.amount += ix.amount;
328
- return finalTx;
329
- }, {});
330
- } // How tokens tx are parsed:
331
- // 0. compute incoming or outgoing tx: it's outgoing if spl-token:transfer has source/destination included in tokenAccountsByOwner
332
- // 1. if it's a sent tx: sum all instructions amount (spl-token:transfer)
333
- // 2. if it's an incoming tx: sull all the amounts with destination included in tokenAccountsByOwner (aggregating by ticker)
334
- // QUESTION: How do I know what are my tokens addresses deterministically? It's not possible, gotta use tokenAccountsByOwner
335
-
336
-
337
- return _objectSpread({
338
- id: txDetails.transaction.signatures[0],
339
- slot: txDetails.slot,
340
- error: !(txDetails.meta.err === null)
341
- }, tx);
342
- }
343
-
344
- async getTokenAccountsByOwner(address, tokenTicker) {
345
- const {
346
- value: accountsList
347
- } = await this.api.post({
348
- method: 'getTokenAccountsByOwner',
349
- params: [address, {
350
- programId: _solanaLib.TOKEN_PROGRAM_ID.toBase58()
351
- }, {
352
- encoding: 'jsonParsed'
353
- }]
354
- });
355
- const tokenAccounts = [];
356
-
357
- for (let entry of accountsList) {
358
- const {
359
- pubkey,
360
- account
361
- } = entry;
362
-
363
- const mint = _lodash.default.get(account, 'data.parsed.info.mint');
364
-
365
- const token = _solanaLib.tokens.find(({
366
- mintAddress
367
- }) => mintAddress === mint) || {
368
- tokenName: 'unknown',
369
- tokenSymbol: 'UNKNOWN'
370
- };
371
-
372
- const balance = _lodash.default.get(account, 'data.parsed.info.tokenAmount.amount', '0');
373
-
374
- tokenAccounts.push({
375
- tokenAccountAddress: pubkey,
376
- owner: address,
377
- tokenName: token.tokenName,
378
- ticker: token.tokenSymbol,
379
- balance
380
- });
381
- } // eventually filter by token
382
-
383
-
384
- return tokenTicker ? tokenAccounts.filter(({
385
- ticker
386
- }) => ticker === tokenTicker) : tokenAccounts;
387
- }
388
-
389
- async getTokensBalance(address, filterByTokens = []) {
390
- let accounts = await this.getTokenAccountsByOwner(address); // Tokens
391
-
392
- const tokensBalance = accounts.reduce((acc, {
393
- tokenName,
394
- balance
395
- }) => {
396
- if (tokenName === 'unknown' || filterByTokens.length && !filterByTokens.includes(tokenName)) return acc; // filter by supported tokens only
397
-
398
- if (!acc[tokenName]) acc[tokenName] = Number(balance); // e.g { 'serum': 123 }
399
- else acc[tokenName] += Number(balance); // merge same token account balance
400
-
401
- return acc;
402
- }, {});
403
- return tokensBalance;
404
- }
405
-
406
- async isAssociatedTokenAccountActive(tokenAddress) {
407
- // Returns the token balance of an SPL Token account.
408
- try {
409
- await this.api.post({
410
- method: 'getTokenAccountBalance',
411
- params: [tokenAddress]
412
- });
413
- return true;
414
- } catch (e) {
415
- return false;
416
- }
417
- }
418
-
419
- async getAddressType(address) {
420
- // solana, token or null (unknown), meaning address has never been initialized
421
- const {
422
- value
423
- } = await this.api.post({
424
- method: 'getAccountInfo',
425
- params: [address, {
426
- encoding: 'base64'
427
- }]
428
- });
429
- if (value === null) return null;
430
- const account = {
431
- executable: value.executable,
432
- owner: value.owner,
433
- lamports: value.lamports,
434
- data: value.data
435
- };
436
- return account.owner === _solanaLib.SYSTEM_PROGRAM_ID.toBase58() ? 'solana' : account.owner === _solanaLib.TOKEN_PROGRAM_ID.toBase58() ? 'token' : null;
437
- }
438
-
439
- async isTokenAddress(address) {
440
- const type = await this.getAddressType(address);
441
- return type === 'token';
442
- }
443
-
444
- async isSOLaddress(address) {
445
- const type = await this.getAddressType(address);
446
- return type === 'solana';
447
- }
448
-
449
- async getStakeAccountsInfo(address) {
450
- // get staked amount and other info
451
- const res = await this.api.post({
452
- method: 'getProgramAccounts',
453
- params: [_solanaLib.STAKE_PROGRAM_ID.toBase58(), {
454
- filters: [{
455
- memcmp: {
456
- offset: 12,
457
- bytes: address
458
- }
459
- }],
460
- encoding: 'jsonParsed'
461
- }]
462
- });
463
- const accounts = {};
464
- let totalStake = 0;
465
- let locked = 0;
466
- let withdrawable = 0;
467
- let pending = 0;
468
-
469
- for (let entry of res) {
470
- const addr = entry.pubkey;
471
-
472
- const lamports = _lodash.default.get(entry, 'account.lamports', 0);
473
-
474
- 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
475
-
476
-
477
- accounts[addr] = delegation;
478
- accounts[addr].lamports = lamports; // sol balance
479
-
480
- accounts[addr].activationEpoch = Number(accounts[addr].activationEpoch) || 0;
481
- accounts[addr].deactivationEpoch = Number(accounts[addr].deactivationEpoch) || 0;
482
- let state = 'inactive';
483
- if (delegation.activationEpoch) state = await this.getStakeActivation(addr);
484
- accounts[addr].state = state;
485
- accounts[addr].isDeactivating = state === 'deactivating';
486
- accounts[addr].canWithdraw = state === 'inactive';
487
- accounts[addr].stake = Number(accounts[addr].stake) || 0; // active staked amount
488
-
489
- totalStake += accounts[addr].stake;
490
- locked += ['active', 'activating'].includes(accounts[addr].state) ? lamports : 0;
491
- withdrawable += accounts[addr].canWithdraw ? lamports : 0;
492
- pending += accounts[addr].isDeactivating ? lamports : 0;
493
- }
494
-
495
- return {
496
- accounts,
497
- totalStake,
498
- locked,
499
- withdrawable,
500
- pending
501
- };
502
- }
503
- /**
504
- * Broadcast a signed transaction
505
- */
506
-
507
-
508
- }
509
-
510
- var _default = new Api();
511
-
512
- exports.default = _default;