@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.
- package/package.json +6 -5
- package/src/index.js +428 -0
- 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.
|
|
3
|
+
"version": "1.2.10",
|
|
4
4
|
"description": "Exodus internal Solana asset API wrapper",
|
|
5
|
-
"main": "
|
|
5
|
+
"main": "src/index.js",
|
|
6
6
|
"files": [
|
|
7
|
-
"
|
|
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.
|
|
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;
|