@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.
- package/README.md +4 -0
- package/package.json +24 -0
- package/src/index.js +241 -0
package/README.md
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
# Solana Api · [](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
|
+
}
|