@exodus/solana-api 1.2.9-build2 → 1.2.10

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