@exodus/solana-api 1.0.1

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/README.md +4 -0
  2. package/package.json +24 -0
  3. package/src/index.js +241 -0
package/README.md ADDED
@@ -0,0 +1,4 @@
1
+ # Solana Api · [![npm version](https://img.shields.io/badge/npm-private-blue.svg?style=flat)](https://www.npmjs.com/package/@exodus/solana-api)
2
+
3
+ - To get all transactions data from an address we gotta call 3 rpcs `getConfirmedSignaturesForAddress2` (get txIds) -> `getConfirmedTransaction` (get tx details) -> `getBlockTime` (get tx timestamp). Pretty annoying and resource-consuming backend-side. (https://github.com/solana-labs/solana/issues/12411)
4
+ - calling `getBlockTime` might results in an error if the slot/block requested is too old (https://github.com/solana-labs/solana/issues/12413), looks like some Solana validators can choose to not keep all the ledger blocks (fix in progress by solana team).
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@exodus/solana-api",
3
+ "version": "1.0.1",
4
+ "description": "Exodus internal Solana asset API wrapper",
5
+ "main": "src/index.js",
6
+ "files": [
7
+ "src/",
8
+ "!src/__tests__"
9
+ ],
10
+ "author": "Exodus",
11
+ "license": "ISC",
12
+ "publishConfig": {
13
+ "access": "restricted"
14
+ },
15
+ "dependencies": {
16
+ "fetchival": "~0.3.2",
17
+ "lodash": "^4.17.11",
18
+ "ms": "~0.7.1"
19
+ },
20
+ "devDependencies": {
21
+ "node-fetch": "~1.6.3"
22
+ },
23
+ "gitHead": "7afc2c44aff00085db4d14b7696a3fc88f165628"
24
+ }
package/src/index.js ADDED
@@ -0,0 +1,241 @@
1
+ // @flow
2
+ import ms from 'ms'
3
+ import fetchival from 'fetchival'
4
+
5
+ // Doc: https://docs.solana.com/apps/jsonrpc-api
6
+
7
+ const RPC_URL = 'https://api.mainnet-beta.solana.com' // https://solana-api.projectserum.com
8
+
9
+ class Api {
10
+ constructor(rpcUrl) {
11
+ this.setServer(rpcUrl)
12
+ }
13
+
14
+ setServer(rpcUrl) {
15
+ this.rpcUrl = rpcUrl || RPC_URL
16
+ }
17
+
18
+ request(path) {
19
+ return fetchival(this.rpcUrl, {
20
+ timeout: ms('15s'),
21
+ headers: { 'Content-Type': 'application/json' },
22
+ })(path)
23
+ }
24
+
25
+ async getRecentBlockHash() {
26
+ try {
27
+ const { result, error } = await this.request('').post({
28
+ jsonrpc: '2.0',
29
+ id: getCurrentTime(),
30
+ method: 'getRecentBlockhash',
31
+ params: [],
32
+ })
33
+ if (error) throw new Error(error.message)
34
+ return result.value.blockhash
35
+ } catch (err) {
36
+ console.log(err)
37
+ throw new Error(JSON.stringify(await concatStream(err.response)))
38
+ }
39
+ }
40
+
41
+ // Transaction structure: https://docs.solana.com/apps/jsonrpc-api#transaction-structure
42
+ async getTransactionById(id: string) {
43
+ try {
44
+ const { result, error } = await this.request('').post({
45
+ jsonrpc: '2.0',
46
+ id: getCurrentTime(),
47
+ method: 'getConfirmedTransaction',
48
+ params: [id, 'json'],
49
+ })
50
+ if (error) throw new Error(error.message)
51
+ return result
52
+ } catch (err) {
53
+ console.log(err)
54
+ throw new Error(JSON.stringify(await concatStream(err.response)))
55
+ }
56
+ }
57
+
58
+ async getFee(): number {
59
+ try {
60
+ const { result, error } = await this.request('').post({
61
+ jsonrpc: '2.0',
62
+ id: getCurrentTime(),
63
+ method: 'getFees',
64
+ params: [],
65
+ })
66
+ if (error) throw new Error(error.message)
67
+ return result.value.feeCalculator.lamportsPerSignature
68
+ } catch (err) {
69
+ throw new Error(JSON.stringify(await concatStream(err.response)))
70
+ }
71
+ }
72
+
73
+ async getBalance(address: string): number {
74
+ try {
75
+ const { result, error } = await this.request('').post({
76
+ jsonrpc: '2.0',
77
+ id: getCurrentTime(),
78
+ method: 'getBalance',
79
+ params: [address],
80
+ })
81
+ if (error) throw new Error(error.message)
82
+ return result.value || 0
83
+ } catch (err) {
84
+ console.log(err)
85
+ throw new Error(JSON.stringify(await concatStream(err.response)))
86
+ }
87
+ }
88
+
89
+ async getBlockTime(slot: number): any {
90
+ try {
91
+ const { result, error } = await this.request('').post({
92
+ jsonrpc: '2.0',
93
+ id: getCurrentTime(),
94
+ method: 'getBlockTime',
95
+ params: [slot],
96
+ })
97
+ // might result in error if executed on a validator with partial ledger (https://github.com/solana-labs/solana/issues/12413)
98
+ if (error) console.log(error.message)
99
+ return result
100
+ } catch (err) {
101
+ console.log(err)
102
+ throw new Error(JSON.stringify(await concatStream(err.response)))
103
+ }
104
+ }
105
+
106
+ async getConfirmedSignaturesForAddress(address: string, { before, until, limit }): any {
107
+ try {
108
+ until = until || undefined
109
+ const { result, error } = await this.request('').post({
110
+ jsonrpc: '2.0',
111
+ id: getCurrentTime(),
112
+ method: 'getConfirmedSignaturesForAddress2',
113
+ params: [address, { before, until, limit }],
114
+ })
115
+ if (error) throw new Error(error.message)
116
+ return result
117
+ } catch (err) {
118
+ console.log(err)
119
+ throw new Error(JSON.stringify(await concatStream(err.response)))
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Get transactions from an address
125
+ */
126
+ async getTransactions(address: string, { cursor, limit } = {}): any {
127
+ let transactions = []
128
+ // cursor is a txHash
129
+
130
+ try {
131
+ let until = cursor
132
+
133
+ const txsId = await this.getConfirmedSignaturesForAddress(address, {
134
+ until,
135
+ limit,
136
+ })
137
+
138
+ for (let tx of txsId) {
139
+ // get tx details
140
+ const txDetails = await this.getTransactionById(tx.signature)
141
+ if (txDetails === null) continue
142
+
143
+ const timestamp = (await this.getBlockTime(tx.slot)) * 1000
144
+
145
+ const from = txDetails.transaction.message.accountKeys[0]
146
+ const to = txDetails.transaction.message.accountKeys[1]
147
+ let { preBalances, postBalances, fee } = txDetails.meta
148
+ const isSending = address === from
149
+ fee = !isSending ? 0 : fee
150
+ const amount = Math.abs(
151
+ isSending ? preBalances[0] - postBalances[0] - fee : postBalances[1] - preBalances[1]
152
+ )
153
+
154
+ transactions.push({
155
+ id: tx.signature,
156
+ memo: tx.memo,
157
+ slot: tx.slot,
158
+ timestamp,
159
+ date: new Date(timestamp),
160
+ fee, // lamports
161
+ from,
162
+ to,
163
+ amount, // lamports
164
+ error: !(txDetails.meta.status.Ok === null),
165
+ })
166
+ }
167
+ } catch (err) {
168
+ console.warn('Solana error:', err)
169
+ if (err.response) {
170
+ const error = new Error(JSON.stringify(await concatStream(err.response)))
171
+ error.status = err.response.status
172
+ error.statusText = err.response.statusText
173
+ throw error
174
+ }
175
+ throw err
176
+ }
177
+
178
+ const newCursor = transactions[0] ? transactions[0].id : cursor
179
+
180
+ return { transactions, newCursor }
181
+ }
182
+
183
+ /**
184
+ * Broadcast a signed transaction
185
+ */
186
+ broadcastTransaction = async (signedTx: string): string => {
187
+ console.log('Solana broadcasting TX:', signedTx)
188
+
189
+ const { result, error } = await this.request('')
190
+ .post({
191
+ jsonrpc: '2.0',
192
+ id: getCurrentTime(),
193
+ method: 'sendTransaction',
194
+ params: [signedTx],
195
+ })
196
+ .catch(async (err) => {
197
+ throw new Error(JSON.stringify(await concatStream(err.response)))
198
+ })
199
+
200
+ if (error) throw new Error(error.message)
201
+
202
+ console.log(`tx ${result} sent!`)
203
+ return result || null
204
+ }
205
+ }
206
+
207
+ export default new Api()
208
+
209
+ function getCurrentTime() {
210
+ const date = new Date()
211
+ return date.getTime()
212
+ }
213
+
214
+ function concatStream(response) {
215
+ const stream = response.body
216
+ if (typeof stream !== 'object')
217
+ throw new Error(`${response.status} - ${response.statusText || 'Server Error'}`)
218
+ if ('locked' in stream) {
219
+ // fetch browser
220
+ try {
221
+ return response.json()
222
+ } catch (e) {
223
+ return response.text()
224
+ }
225
+ }
226
+ return new Promise((resolve, reject) => {
227
+ let result = ''
228
+ stream.on('data', (chunk) => {
229
+ result += chunk.toString()
230
+ })
231
+ stream.on('end', () => {
232
+ try {
233
+ const parsed = JSON.parse(result)
234
+ resolve(parsed)
235
+ } catch (e) {
236
+ resolve(result) // plain text
237
+ }
238
+ })
239
+ stream.on('error', (err) => reject(err))
240
+ })
241
+ }