@exodus/solana-api 1.2.0 → 1.2.2
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 +3 -2
- package/src/index.js +159 -21
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/solana-api",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"description": "Exodus internal Solana asset API wrapper",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"files": [
|
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
"access": "restricted"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
+
"@exodus/solana-lib": "^1.2.1",
|
|
16
17
|
"@exodus/solana-web3.js": "^0.87.1-exodus3",
|
|
17
18
|
"lodash": "^4.17.11"
|
|
18
19
|
},
|
|
19
|
-
"gitHead": "
|
|
20
|
+
"gitHead": "e599da7c7470f8bd73b9902e267736d5543bf27d"
|
|
20
21
|
}
|
package/src/index.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
// @flow
|
|
2
2
|
import { Connection, PublicKey } from '@exodus/solana-web3.js'
|
|
3
|
+
import { tokens, SYSTEM_PROGRAM_ID, TOKEN_PROGRAM_ID } from '@exodus/solana-lib'
|
|
4
|
+
import assert from 'assert'
|
|
5
|
+
import lodash from 'lodash'
|
|
3
6
|
|
|
4
7
|
// Doc: https://solana-labs.github.io/solana-web3.js/
|
|
5
8
|
|
|
6
9
|
const RPC_URL = 'https://api.mainnet-beta.solana.com' // https://solana-api.projectserum.com
|
|
7
10
|
|
|
11
|
+
// Tokens + SOL api support
|
|
8
12
|
class Api {
|
|
9
13
|
constructor(rpcUrl) {
|
|
10
14
|
this.setServer(rpcUrl)
|
|
@@ -22,7 +26,11 @@ class Api {
|
|
|
22
26
|
|
|
23
27
|
// Transaction structure: https://docs.solana.com/apps/jsonrpc-api#transaction-structure
|
|
24
28
|
async getTransactionById(id: string) {
|
|
25
|
-
|
|
29
|
+
const { result } = await this.connection._rpcRequest('getConfirmedTransaction', [
|
|
30
|
+
id,
|
|
31
|
+
'jsonParsed',
|
|
32
|
+
])
|
|
33
|
+
return result
|
|
26
34
|
}
|
|
27
35
|
|
|
28
36
|
async getFee(): number {
|
|
@@ -41,11 +49,11 @@ class Api {
|
|
|
41
49
|
return this.connection.getBlockTime(slot)
|
|
42
50
|
}
|
|
43
51
|
|
|
44
|
-
async getConfirmedSignaturesForAddress(address: string, {
|
|
52
|
+
async getConfirmedSignaturesForAddress(address: string, { until, before, limit } = {}): any {
|
|
45
53
|
until = until || undefined
|
|
46
54
|
return this.connection.getConfirmedSignaturesForAddress2(new PublicKey(address), {
|
|
47
|
-
before,
|
|
48
55
|
until,
|
|
56
|
+
before,
|
|
49
57
|
limit,
|
|
50
58
|
})
|
|
51
59
|
}
|
|
@@ -53,7 +61,7 @@ class Api {
|
|
|
53
61
|
/**
|
|
54
62
|
* Get transactions from an address
|
|
55
63
|
*/
|
|
56
|
-
async getTransactions(address: string, { cursor, limit } = {}): any {
|
|
64
|
+
async getTransactions(address: string, { cursor, before, limit, tokenTicker } = {}): any {
|
|
57
65
|
let transactions = []
|
|
58
66
|
// cursor is a txHash
|
|
59
67
|
|
|
@@ -62,9 +70,17 @@ class Api {
|
|
|
62
70
|
|
|
63
71
|
const txsId = await this.getConfirmedSignaturesForAddress(address, {
|
|
64
72
|
until,
|
|
73
|
+
before,
|
|
65
74
|
limit,
|
|
66
75
|
})
|
|
67
76
|
|
|
77
|
+
let tokenAccountsByOwner // Array
|
|
78
|
+
if (txsId.length) tokenAccountsByOwner = await this.getTokenAccountsByOwner(address)
|
|
79
|
+
|
|
80
|
+
if (tokenTicker) {
|
|
81
|
+
// TODO: eventually filter txs by token ticker (is it done at monitor level?)
|
|
82
|
+
}
|
|
83
|
+
|
|
68
84
|
for (let tx of txsId) {
|
|
69
85
|
// get tx details
|
|
70
86
|
const [txDetails, blockTime] = await Promise.all([
|
|
@@ -74,26 +90,10 @@ class Api {
|
|
|
74
90
|
if (txDetails === null) continue
|
|
75
91
|
|
|
76
92
|
const timestamp = blockTime * 1000
|
|
77
|
-
const from = txDetails.transaction.keys[0].toString()
|
|
78
|
-
const to = txDetails.transaction.keys[1].toString()
|
|
79
|
-
let { preBalances, postBalances, fee } = txDetails.meta
|
|
80
|
-
const isSending = address === from
|
|
81
|
-
fee = !isSending ? 0 : fee
|
|
82
|
-
const amount = Math.abs(
|
|
83
|
-
isSending ? preBalances[0] - postBalances[0] - fee : postBalances[1] - preBalances[1]
|
|
84
|
-
)
|
|
85
|
-
|
|
86
93
|
transactions.push({
|
|
87
|
-
id: tx.signature,
|
|
88
|
-
memo: tx.memo,
|
|
89
|
-
slot: tx.slot,
|
|
90
94
|
timestamp,
|
|
91
95
|
date: new Date(timestamp),
|
|
92
|
-
|
|
93
|
-
from,
|
|
94
|
-
to,
|
|
95
|
-
amount, // lamports
|
|
96
|
-
error: !(txDetails.meta.err === null),
|
|
96
|
+
...Api.parseTransaction(address, txDetails, tokenAccountsByOwner),
|
|
97
97
|
})
|
|
98
98
|
}
|
|
99
99
|
} catch (err) {
|
|
@@ -106,6 +106,144 @@ class Api {
|
|
|
106
106
|
return { transactions, newCursor }
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
parseTransaction(...args) {
|
|
110
|
+
// alias
|
|
111
|
+
return Api.parseTransaction(...args)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
static parseTransaction(
|
|
115
|
+
ownerAddress: string,
|
|
116
|
+
txDetails: Object,
|
|
117
|
+
tokenAccountsByOwner: ?Array
|
|
118
|
+
): Object {
|
|
119
|
+
const { fee } = txDetails.meta
|
|
120
|
+
let { instructions } = txDetails.transaction.message
|
|
121
|
+
instructions = instructions
|
|
122
|
+
.filter((ix) => ix.parsed) // only known instructions
|
|
123
|
+
.map((ix) => ({
|
|
124
|
+
program: ix.program, // system or spl-token
|
|
125
|
+
type: ix.parsed.type, // transfer, createAccount, initializeAccount
|
|
126
|
+
...ix.parsed.info,
|
|
127
|
+
}))
|
|
128
|
+
|
|
129
|
+
const solanaTx = lodash.find(instructions, { program: 'system', type: 'transfer' }) // get SOL transfer
|
|
130
|
+
// program:type tells us if it's a SOL or Token transfer
|
|
131
|
+
|
|
132
|
+
let tx
|
|
133
|
+
if (solanaTx) {
|
|
134
|
+
// Solana tx
|
|
135
|
+
const isSending = ownerAddress === solanaTx.source
|
|
136
|
+
tx = {
|
|
137
|
+
from: solanaTx.source,
|
|
138
|
+
to: solanaTx.destination,
|
|
139
|
+
amount: solanaTx.lamports, // number
|
|
140
|
+
fee: isSending ? fee : 0,
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
143
|
+
// Token tx
|
|
144
|
+
assert.ok(
|
|
145
|
+
Array.isArray(tokenAccountsByOwner),
|
|
146
|
+
'tokenAccountsByOwner is required when parsing token tx'
|
|
147
|
+
)
|
|
148
|
+
let tokenTxs = lodash
|
|
149
|
+
.filter(instructions, { program: 'spl-token', type: 'transfer' }) // get Token transfer: could have more than 1 instructions
|
|
150
|
+
.map((ix) => {
|
|
151
|
+
// add token details based on source/destination address
|
|
152
|
+
let tokenAccount = lodash.find(tokenAccountsByOwner, { tokenAccountAddress: ix.source })
|
|
153
|
+
const isSending = !!tokenAccount
|
|
154
|
+
if (!isSending)
|
|
155
|
+
tokenAccount = lodash.find(tokenAccountsByOwner, {
|
|
156
|
+
tokenAccountAddress: ix.destination,
|
|
157
|
+
}) // receiving
|
|
158
|
+
if (!tokenAccount) return null // no transfers with our addresses involved
|
|
159
|
+
delete tokenAccount.balance
|
|
160
|
+
delete tokenAccount.owner
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
token: tokenAccount,
|
|
164
|
+
from: ix.source,
|
|
165
|
+
to: ix.destination,
|
|
166
|
+
amount: Number(ix.amount), // token
|
|
167
|
+
fee: isSending ? fee : 0, // in lamports
|
|
168
|
+
}
|
|
169
|
+
})
|
|
170
|
+
// .reduce to sum/sub (based on isSending) all the same tokens amount (From instructions -> 1 single tx)
|
|
171
|
+
tx = tokenTxs.reduce((finalTx, ix) => {
|
|
172
|
+
if (!ix) return finalTx // skip null instructions
|
|
173
|
+
if (!finalTx.token) return ix // init finalTx (support just 1 token type per tx)
|
|
174
|
+
if (finalTx.token.ticker === ix.token.ticker) finalTx.amount += ix.amount
|
|
175
|
+
return finalTx
|
|
176
|
+
}, {})
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// How tokens tx are parsed:
|
|
180
|
+
// 0. compute incoming or outgoing tx: it's outgoing if spl-token:transfer has source/destination included in tokenAccountsByOwner
|
|
181
|
+
// 1. if it's a sent tx: sum all instructions amount (spl-token:transfer)
|
|
182
|
+
// 2. if it's an incoming tx: sull all the amounts with destination included in tokenAccountsByOwner (aggregating by ticker)
|
|
183
|
+
// QUESTION: How do I know what are my tokens addresses deterministically? It's not possible, gotta use tokenAccountsByOwner
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
id: txDetails.transaction.signatures[0],
|
|
187
|
+
slot: txDetails.slot,
|
|
188
|
+
error: !(txDetails.meta.err === null),
|
|
189
|
+
...tx,
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async getTokenAccountsByOwner(address: string, tokenTicker: ?string): Array {
|
|
194
|
+
const {
|
|
195
|
+
result: { value: accountsList },
|
|
196
|
+
} = await this.connection._rpcRequest('getTokenAccountsByOwner', [
|
|
197
|
+
address,
|
|
198
|
+
{ programId: TOKEN_PROGRAM_ID.toBase58() },
|
|
199
|
+
{ encoding: 'jsonParsed' },
|
|
200
|
+
])
|
|
201
|
+
|
|
202
|
+
const tokenAccounts = []
|
|
203
|
+
for (let entry of accountsList) {
|
|
204
|
+
const { pubkey, account } = entry
|
|
205
|
+
|
|
206
|
+
const mint = lodash.get(account, 'data.parsed.info.mint')
|
|
207
|
+
const token = tokens.find(({ mintAddress }) => mintAddress === mint) || {
|
|
208
|
+
tokenName: 'unknown',
|
|
209
|
+
tokenSymbol: 'UNKNOWN',
|
|
210
|
+
}
|
|
211
|
+
const balance = lodash.get(account, 'data.parsed.info.tokenAmount.amount', '0')
|
|
212
|
+
tokenAccounts.push({
|
|
213
|
+
tokenAccountAddress: pubkey,
|
|
214
|
+
owner: address,
|
|
215
|
+
tokenName: token.tokenName,
|
|
216
|
+
ticker: token.tokenSymbol,
|
|
217
|
+
balance,
|
|
218
|
+
})
|
|
219
|
+
}
|
|
220
|
+
// eventually filter by token
|
|
221
|
+
return tokenTicker
|
|
222
|
+
? tokenAccounts.filter(({ ticker }) => ticker === tokenTicker)
|
|
223
|
+
: tokenAccounts
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async getAddressType(address: string) {
|
|
227
|
+
// solana, token or null (unknown), meaning address has never been initialized
|
|
228
|
+
const account = await this.connection.getAccountInfo(new PublicKey(address))
|
|
229
|
+
if (account === null) return null
|
|
230
|
+
return account.owner.equals(SYSTEM_PROGRAM_ID)
|
|
231
|
+
? 'solana'
|
|
232
|
+
: account.owner.equals(TOKEN_PROGRAM_ID)
|
|
233
|
+
? 'token'
|
|
234
|
+
: null
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async isTokenAddress(address: string) {
|
|
238
|
+
const type = await this.getAddressType(address)
|
|
239
|
+
return type === 'token'
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async isSOLaddress(address: string) {
|
|
243
|
+
const type = await this.getAddressType(address)
|
|
244
|
+
return type === 'solana'
|
|
245
|
+
}
|
|
246
|
+
|
|
109
247
|
/**
|
|
110
248
|
* Broadcast a signed transaction
|
|
111
249
|
*/
|