@exodus/solana-api 1.2.3 → 1.2.5
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 +9 -5
- package/src/index.js +109 -45
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/solana-api",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.5",
|
|
4
4
|
"description": "Exodus internal Solana asset API wrapper",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"files": [
|
|
@@ -13,9 +13,13 @@
|
|
|
13
13
|
"access": "restricted"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@exodus/
|
|
17
|
-
"@exodus/solana-
|
|
18
|
-
"lodash": "^4.17.11"
|
|
16
|
+
"@exodus/asset-json-rpc": "^1.0.0",
|
|
17
|
+
"@exodus/solana-lib": "^1.2.5",
|
|
18
|
+
"lodash": "^4.17.11",
|
|
19
|
+
"wretch": "^1.5.2"
|
|
19
20
|
},
|
|
20
|
-
"
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"node-fetch": "~1.6.3"
|
|
23
|
+
},
|
|
24
|
+
"gitHead": "791e305a718246460d5ac93ed2eb7808955c0f5d"
|
|
21
25
|
}
|
package/src/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// @flow
|
|
2
|
-
import
|
|
2
|
+
import createApi from '@exodus/asset-json-rpc'
|
|
3
3
|
import { tokens, SYSTEM_PROGRAM_ID, TOKEN_PROGRAM_ID } from '@exodus/solana-lib'
|
|
4
4
|
import assert from 'assert'
|
|
5
5
|
import lodash from 'lodash'
|
|
6
6
|
|
|
7
|
-
// Doc: https://solana
|
|
7
|
+
// Doc: https://docs.solana.com/apps/jsonrpc-api
|
|
8
8
|
|
|
9
|
-
const RPC_URL = 'https://api.mainnet-beta.solana.com' // https://solana-api.projectserum.com
|
|
9
|
+
const RPC_URL = 'https://vip-api.mainnet-beta.solana.com/' // https://api.mainnet-beta.solana.com, https://solana-api.projectserum.com
|
|
10
10
|
|
|
11
11
|
// Tokens + SOL api support
|
|
12
12
|
class Api {
|
|
@@ -16,70 +16,91 @@ class Api {
|
|
|
16
16
|
|
|
17
17
|
setServer(rpcUrl) {
|
|
18
18
|
this.rpcUrl = rpcUrl || RPC_URL
|
|
19
|
-
this.
|
|
19
|
+
this.api = createApi(this.rpcUrl)
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
async getRecentBlockHash(): string {
|
|
23
|
-
const {
|
|
23
|
+
const {
|
|
24
|
+
value: { blockhash },
|
|
25
|
+
} = await this.api.post({
|
|
26
|
+
method: 'getRecentBlockhash',
|
|
27
|
+
})
|
|
24
28
|
return blockhash
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
// Transaction structure: https://docs.solana.com/apps/jsonrpc-api#transaction-structure
|
|
28
32
|
async getTransactionById(id: string) {
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
'jsonParsed',
|
|
32
|
-
|
|
33
|
+
const result = await this.api.post({
|
|
34
|
+
method: 'getConfirmedTransaction',
|
|
35
|
+
params: [id, 'jsonParsed'],
|
|
36
|
+
})
|
|
33
37
|
return result
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
async getFee(): number {
|
|
37
41
|
const {
|
|
38
|
-
|
|
39
|
-
|
|
42
|
+
value: {
|
|
43
|
+
feeCalculator: { lamportsPerSignature },
|
|
44
|
+
},
|
|
45
|
+
} = await this.api.post({
|
|
46
|
+
method: 'getRecentBlockhash',
|
|
47
|
+
})
|
|
40
48
|
return lamportsPerSignature
|
|
41
49
|
}
|
|
42
50
|
|
|
43
51
|
async getBalance(address: string): number {
|
|
44
|
-
|
|
52
|
+
const res = await this.api.post({
|
|
53
|
+
method: 'getBalance',
|
|
54
|
+
params: [address],
|
|
55
|
+
})
|
|
56
|
+
return res.value || 0
|
|
45
57
|
}
|
|
46
58
|
|
|
47
59
|
async getBlockTime(slot: number) {
|
|
48
60
|
// might result in error if executed on a validator with partial ledger (https://github.com/solana-labs/solana/issues/12413)
|
|
49
|
-
return this.
|
|
61
|
+
return this.api.post({
|
|
62
|
+
method: 'getBlockTime',
|
|
63
|
+
params: [slot],
|
|
64
|
+
})
|
|
50
65
|
}
|
|
51
66
|
|
|
52
67
|
async getConfirmedSignaturesForAddress(address: string, { until, before, limit } = {}): any {
|
|
53
68
|
until = until || undefined
|
|
54
|
-
return this.
|
|
55
|
-
|
|
56
|
-
before,
|
|
57
|
-
limit,
|
|
69
|
+
return this.api.post({
|
|
70
|
+
method: 'getConfirmedSignaturesForAddress2',
|
|
71
|
+
params: [address, { until, before, limit }],
|
|
58
72
|
})
|
|
59
73
|
}
|
|
60
74
|
|
|
61
75
|
/**
|
|
62
76
|
* Get transactions from an address
|
|
63
77
|
*/
|
|
64
|
-
async getTransactions(address: string, { cursor, before, limit
|
|
78
|
+
async getTransactions(address: string, { cursor, before, limit } = {}): any {
|
|
65
79
|
let transactions = []
|
|
66
80
|
// cursor is a txHash
|
|
67
81
|
|
|
68
82
|
try {
|
|
69
83
|
let until = cursor
|
|
70
84
|
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
85
|
+
const tokenAccountsByOwner = await this.getTokenAccountsByOwner(address) // Array
|
|
86
|
+
const tokenAccountAddresses = tokenAccountsByOwner
|
|
87
|
+
.filter(({ tokenName }) => tokenName !== 'unknown')
|
|
88
|
+
.map(({ tokenAccountAddress }) => tokenAccountAddress)
|
|
89
|
+
const accountsToCheck = [address, ...tokenAccountAddresses]
|
|
90
|
+
|
|
91
|
+
console.log('accountsToCheck::', accountsToCheck)
|
|
92
|
+
|
|
93
|
+
const txsResultsByAccount = await Promise.all(
|
|
94
|
+
accountsToCheck.map((addr) =>
|
|
95
|
+
this.getConfirmedSignaturesForAddress(addr, {
|
|
96
|
+
until,
|
|
97
|
+
before,
|
|
98
|
+
limit,
|
|
99
|
+
})
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
let txsId = txsResultsByAccount.reduce((arr, row) => arr.concat(row), []) // merge arrays
|
|
103
|
+
txsId = lodash.uniqBy(txsId, 'signature')
|
|
83
104
|
|
|
84
105
|
for (let tx of txsId) {
|
|
85
106
|
// get tx details
|
|
@@ -101,6 +122,8 @@ class Api {
|
|
|
101
122
|
throw err
|
|
102
123
|
}
|
|
103
124
|
|
|
125
|
+
transactions = lodash.orderBy(transactions, ['timestamp'], ['desc'])
|
|
126
|
+
|
|
104
127
|
const newCursor = transactions[0] ? transactions[0].id : cursor
|
|
105
128
|
|
|
106
129
|
return { transactions, newCursor }
|
|
@@ -134,6 +157,7 @@ class Api {
|
|
|
134
157
|
// Solana tx
|
|
135
158
|
const isSending = ownerAddress === solanaTx.source
|
|
136
159
|
tx = {
|
|
160
|
+
owner: solanaTx.source,
|
|
137
161
|
from: solanaTx.source,
|
|
138
162
|
to: solanaTx.destination,
|
|
139
163
|
amount: solanaTx.lamports, // number
|
|
@@ -156,10 +180,12 @@ class Api {
|
|
|
156
180
|
tokenAccountAddress: ix.destination,
|
|
157
181
|
}) // receiving
|
|
158
182
|
if (!tokenAccount) return null // no transfers with our addresses involved
|
|
183
|
+
const owner = isSending ? ownerAddress : null
|
|
184
|
+
|
|
159
185
|
delete tokenAccount.balance
|
|
160
186
|
delete tokenAccount.owner
|
|
161
|
-
|
|
162
187
|
return {
|
|
188
|
+
owner,
|
|
163
189
|
token: tokenAccount,
|
|
164
190
|
from: ix.source,
|
|
165
191
|
to: ix.destination,
|
|
@@ -167,6 +193,7 @@ class Api {
|
|
|
167
193
|
fee: isSending ? fee : 0, // in lamports
|
|
168
194
|
}
|
|
169
195
|
})
|
|
196
|
+
|
|
170
197
|
// .reduce to sum/sub (based on isSending) all the same tokens amount (From instructions -> 1 single tx)
|
|
171
198
|
tx = tokenTxs.reduce((finalTx, ix) => {
|
|
172
199
|
if (!ix) return finalTx // skip null instructions
|
|
@@ -191,13 +218,10 @@ class Api {
|
|
|
191
218
|
}
|
|
192
219
|
|
|
193
220
|
async getTokenAccountsByOwner(address: string, tokenTicker: ?string): Array {
|
|
194
|
-
const {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
{ programId: TOKEN_PROGRAM_ID.toBase58() },
|
|
199
|
-
{ encoding: 'jsonParsed' },
|
|
200
|
-
])
|
|
221
|
+
const { value: accountsList } = await this.api.post({
|
|
222
|
+
method: 'getTokenAccountsByOwner',
|
|
223
|
+
params: [address, { programId: TOKEN_PROGRAM_ID.toBase58() }, { encoding: 'jsonParsed' }],
|
|
224
|
+
})
|
|
201
225
|
|
|
202
226
|
const tokenAccounts = []
|
|
203
227
|
for (let entry of accountsList) {
|
|
@@ -223,13 +247,51 @@ class Api {
|
|
|
223
247
|
: tokenAccounts
|
|
224
248
|
}
|
|
225
249
|
|
|
250
|
+
async getTokensBalance(address: string) {
|
|
251
|
+
let accounts = await this.getTokenAccountsByOwner(address) // Tokens
|
|
252
|
+
|
|
253
|
+
const tokensBalance = accounts.reduce((acc, { tokenName, balance }) => {
|
|
254
|
+
if (tokenName === 'unknown') return acc // filter by supported tokens only
|
|
255
|
+
if (!acc[tokenName]) acc[tokenName] = Number(balance)
|
|
256
|
+
// e.g { 'serum': 123 }
|
|
257
|
+
else acc[tokenName] += Number(balance) // merge same token account balance
|
|
258
|
+
return acc
|
|
259
|
+
}, {})
|
|
260
|
+
|
|
261
|
+
return tokensBalance
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async isAssociatedTokenAccountActive(tokenAddress: string) {
|
|
265
|
+
// Returns the token balance of an SPL Token account.
|
|
266
|
+
try {
|
|
267
|
+
await this.api.post({
|
|
268
|
+
method: 'getTokenAccountBalance',
|
|
269
|
+
params: [tokenAddress],
|
|
270
|
+
})
|
|
271
|
+
return true
|
|
272
|
+
} catch (e) {
|
|
273
|
+
return false
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
226
277
|
async getAddressType(address: string) {
|
|
227
278
|
// solana, token or null (unknown), meaning address has never been initialized
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
279
|
+
const { value } = await this.api.post({
|
|
280
|
+
method: 'getAccountInfo',
|
|
281
|
+
params: [address, { encoding: 'base64' }],
|
|
282
|
+
})
|
|
283
|
+
if (value === null) return null
|
|
284
|
+
|
|
285
|
+
const account = {
|
|
286
|
+
executable: value.executable,
|
|
287
|
+
owner: value.owner,
|
|
288
|
+
lamports: value.lamports,
|
|
289
|
+
data: value.data,
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return account.owner === SYSTEM_PROGRAM_ID.toBase58()
|
|
231
293
|
? 'solana'
|
|
232
|
-
: account.owner.
|
|
294
|
+
: account.owner === TOKEN_PROGRAM_ID.toBase58()
|
|
233
295
|
? 'token'
|
|
234
296
|
: null
|
|
235
297
|
}
|
|
@@ -250,10 +312,12 @@ class Api {
|
|
|
250
312
|
broadcastTransaction = async (signedTx: string): string => {
|
|
251
313
|
console.log('Solana broadcasting TX:', signedTx) // base64
|
|
252
314
|
|
|
253
|
-
const
|
|
254
|
-
|
|
315
|
+
const result = await this.api.post({
|
|
316
|
+
method: 'sendTransaction',
|
|
317
|
+
params: [signedTx, { encoding: 'base64' }],
|
|
318
|
+
})
|
|
255
319
|
|
|
256
|
-
console.log(`tx ${result} sent!`)
|
|
320
|
+
console.log(`tx ${JSON.stringify(result)} sent!`)
|
|
257
321
|
return result || null
|
|
258
322
|
}
|
|
259
323
|
}
|