@exodus/solana-api 2.0.11 → 2.0.13

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/solana-api",
3
- "version": "2.0.11",
3
+ "version": "2.0.13",
4
4
  "description": "Exodus internal Solana asset API wrapper",
5
5
  "main": "src/index.js",
6
6
  "files": [
@@ -14,13 +14,15 @@
14
14
  },
15
15
  "dependencies": {
16
16
  "@exodus/asset-json-rpc": "^1.0.0",
17
- "@exodus/asset-lib": "^3.6.0",
18
- "@exodus/assets": "^8.0.68",
19
- "@exodus/assets-base": "^8.0.139",
17
+ "@exodus/asset-lib": "^3.6.2",
18
+ "@exodus/assets": "^8.0.71",
19
+ "@exodus/assets-base": "^8.0.150",
20
+ "@exodus/fetch": "^1.2.0",
20
21
  "@exodus/models": "^8.7.2",
21
22
  "@exodus/nfts-core": "^0.5.0",
22
- "@exodus/solana-lib": "^1.3.14",
23
+ "@exodus/solana-lib": "^1.3.15",
23
24
  "bn.js": "^4.11.0",
25
+ "debug": "^4.1.1",
24
26
  "lodash": "^4.17.11",
25
27
  "url-join": "4.0.0",
26
28
  "wretch": "^1.5.2"
@@ -28,5 +30,5 @@
28
30
  "devDependencies": {
29
31
  "node-fetch": "~2.6.0"
30
32
  },
31
- "gitHead": "3c8f16e5f163cd9d09b0b4cfb3ad852aee8a7fe8"
33
+ "gitHead": "3c0551a25d29f31be43d522e742d5754cd7deb5f"
32
34
  }
package/src/api.js CHANGED
@@ -20,17 +20,21 @@ import lodash from 'lodash'
20
20
  import urljoin from 'url-join'
21
21
  import wretch, { Wretcher } from 'wretch'
22
22
  import { magicEden } from '@exodus/nfts-core'
23
+ import { Connection } from './connection'
23
24
 
24
25
  // Doc: https://docs.solana.com/apps/jsonrpc-api
25
26
 
26
27
  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
28
+ const WS_ENDPOINT = 'wss://solana.a.exodus.io/ws'
27
29
 
28
30
  // Tokens + SOL api support
29
31
  export class Api {
30
- constructor(rpcUrl) {
32
+ constructor(rpcUrl, wsUrl) {
31
33
  this.setServer(rpcUrl)
34
+ this.setWsEndpoint(wsUrl)
32
35
  this.setTokens(assets)
33
36
  this.tokensToSkip = {}
37
+ this.connections = {}
34
38
  }
35
39
 
36
40
  setServer(rpcUrl) {
@@ -38,6 +42,10 @@ export class Api {
38
42
  this.api = createApi(this.rpcUrl)
39
43
  }
40
44
 
45
+ setWsEndpoint(wsUrl) {
46
+ this.wsUrl = wsUrl || WS_ENDPOINT
47
+ }
48
+
41
49
  setTokens(assets = {}) {
42
50
  const solTokens = lodash.pickBy(assets, (asset) => asset.assetType === 'SOLANA_TOKEN')
43
51
  this.tokens = lodash.mapKeys(solTokens, (v) => v.mintAddress)
@@ -49,76 +57,100 @@ export class Api {
49
57
  })
50
58
  }
51
59
 
60
+ async watchAddress({
61
+ address,
62
+ tokensAddresses = [],
63
+ handleAccounts,
64
+ handleTransfers,
65
+ handleReconnect,
66
+ reconnectDelay,
67
+ }) {
68
+ const conn = new Connection({
69
+ endpoint: this.wsUrl,
70
+ address,
71
+ tokensAddresses,
72
+ callback: (updates) =>
73
+ this.handleUpdates({ updates, address, handleAccounts, handleTransfers }),
74
+ reconnectCallback: handleReconnect,
75
+ reconnectDelay,
76
+ })
77
+
78
+ this.connections[address] = conn
79
+ return conn.start()
80
+ }
81
+
82
+ async unwatchAddress({ address }) {
83
+ if (this.connections[address]) {
84
+ await this.connections[address].stop()
85
+ delete this.connections[address]
86
+ }
87
+ }
88
+
89
+ async handleUpdates({ updates, address, handleAccounts, handleTransfers }) {
90
+ // console.log(`got ws updates from ${address}:`, updates)
91
+ if (handleTransfers) return handleTransfers(updates)
92
+ }
93
+
94
+ async rpcCall(method, params = [], { address = '', forceHttp = false } = {}) {
95
+ // ws request
96
+ const connection = this.connections[address] || lodash.sample(Object.values(this.connections)) // pick random connection
97
+ if (lodash.get(connection, 'isOpen') && !lodash.get(connection, 'shutdown') && !forceHttp) {
98
+ return connection.sendMessage(method, params)
99
+ }
100
+ // http fallback
101
+ return this.api.post({ method, params })
102
+ }
103
+
52
104
  isTokenSupported(mint: string) {
53
105
  return !!this.tokens[mint]
54
106
  }
55
107
 
56
108
  async getEpochInfo(): number {
57
- const { epoch } = await this.api.post({
58
- method: 'getEpochInfo',
59
- })
109
+ const { epoch } = await this.rpcCall('getEpochInfo')
60
110
  return Number(epoch)
61
111
  }
62
112
 
63
113
  async getStakeActivation(address): string {
64
- const { state } = await this.api.post({
65
- method: 'getStakeActivation',
66
- params: [address],
67
- })
114
+ const { state } = await this.rpcCall('getStakeActivation', [address])
68
115
  return state
69
116
  }
70
117
 
71
118
  async getRecentBlockHash(): string {
72
- const {
73
- value: { blockhash },
74
- } = await this.api.post({
75
- method: 'getRecentBlockhash',
76
- })
77
- return blockhash
119
+ const result = await this.rpcCall(
120
+ 'getRecentBlockhash',
121
+ [{ commitment: 'finalized', encoding: 'jsonParsed' }],
122
+ { forceHttp: true }
123
+ )
124
+ return lodash.get(result, 'value.blockhash')
78
125
  }
79
126
 
80
127
  // Transaction structure: https://docs.solana.com/apps/jsonrpc-api#transaction-structure
81
128
  async getTransactionById(id: string) {
82
- const result = await this.api.post({
83
- method: 'getConfirmedTransaction',
84
- params: [id, 'jsonParsed'],
85
- })
86
- return result
129
+ return this.rpcCall('getConfirmedTransaction', [id, { encoding: 'jsonParsed' }])
87
130
  }
88
131
 
89
132
  async getFee(): number {
90
- const {
91
- value: {
92
- feeCalculator: { lamportsPerSignature },
93
- },
94
- } = await this.api.post({
95
- method: 'getRecentBlockhash',
96
- })
97
- return lamportsPerSignature
133
+ const result = await this.rpcCall('getRecentBlockhash', [
134
+ { commitment: 'finalized', encoding: 'jsonParsed' },
135
+ ])
136
+ return lodash.get(result, 'value.feeCalculator.lamportsPerSignature')
98
137
  }
99
138
 
100
139
  async getBalance(address: string): number {
101
- const res = await this.api.post({
102
- method: 'getBalance',
103
- params: [address],
140
+ const result = await this.rpcCall('getBalance', [address, { encoding: 'jsonParsed' }], {
141
+ address,
104
142
  })
105
- return res.value || 0
143
+ return lodash.get(result, 'value', 0)
106
144
  }
107
145
 
108
146
  async getBlockTime(slot: number) {
109
147
  // might result in error if executed on a validator with partial ledger (https://github.com/solana-labs/solana/issues/12413)
110
- return this.api.post({
111
- method: 'getBlockTime',
112
- params: [slot],
113
- })
148
+ return this.rpcCall('getBlockTime', [slot])
114
149
  }
115
150
 
116
151
  async getConfirmedSignaturesForAddress(address: string, { until, before, limit } = {}): any {
117
152
  until = until || undefined
118
- return this.api.post({
119
- method: 'getSignaturesForAddress',
120
- params: [address, { until, before, limit }],
121
- })
153
+ return this.rpcCall('getSignaturesForAddress', [address, { until, before, limit }], { address })
122
154
  }
123
155
 
124
156
  /**
@@ -221,7 +253,8 @@ export class Api {
221
253
 
222
254
  return {
223
255
  unparsed: true,
224
- amount: postBalances[ownerIndex] - preBalances[ownerIndex] + feePaid,
256
+ amount:
257
+ ownerIndex === -1 ? 0 : postBalances[ownerIndex] - preBalances[ownerIndex] + feePaid,
225
258
  fee: feePaid,
226
259
  data: {
227
260
  meta: txDetails.meta,
@@ -524,13 +557,8 @@ export class Api {
524
557
  }
525
558
 
526
559
  async getSupply(mintAddress: string): string {
527
- const {
528
- value: { amount },
529
- } = await this.api.post({
530
- method: 'getTokenSupply',
531
- params: [mintAddress],
532
- })
533
- return amount
560
+ const result = await this.rpcCall('getTokenSupply', [mintAddress])
561
+ return lodash.get(result, 'value.amount')
534
562
  }
535
563
 
536
564
  async getWalletTokensList({ tokenAccounts }) {
@@ -556,10 +584,11 @@ export class Api {
556
584
  }
557
585
 
558
586
  async getTokenAccountsByOwner(address: string, tokenTicker: ?string): Array {
559
- const { value: accountsList } = await this.api.post({
560
- method: 'getTokenAccountsByOwner',
561
- params: [address, { programId: TOKEN_PROGRAM_ID.toBase58() }, { encoding: 'jsonParsed' }],
562
- })
587
+ const { value: accountsList } = await this.rpcCall(
588
+ 'getTokenAccountsByOwner',
589
+ [address, { programId: TOKEN_PROGRAM_ID.toBase58() }, { encoding: 'jsonParsed' }],
590
+ { address }
591
+ )
563
592
 
564
593
  const tokenAccounts = []
565
594
  for (let entry of accountsList) {
@@ -604,10 +633,7 @@ export class Api {
604
633
  async isAssociatedTokenAccountActive(tokenAddress: string) {
605
634
  // Returns the token balance of an SPL Token account.
606
635
  try {
607
- await this.api.post({
608
- method: 'getTokenAccountBalance',
609
- params: [tokenAddress],
610
- })
636
+ await this.rpcCall('getTokenAccountBalance', [tokenAddress])
611
637
  return true
612
638
  } catch (e) {
613
639
  return false
@@ -616,21 +642,16 @@ export class Api {
616
642
 
617
643
  // Returns account balance of a SPL Token account.
618
644
  async getTokenBalance(tokenAddress: string) {
619
- const {
620
- value: { amount },
621
- } = await this.api.post({
622
- method: 'getTokenAccountBalance',
623
- params: [tokenAddress],
624
- })
625
-
626
- return amount
645
+ const result = await this.rpcCall('getTokenAccountBalance', [tokenAddress])
646
+ return lodash.get(result, 'value.amount')
627
647
  }
628
648
 
629
649
  async getAccountInfo(address: string) {
630
- const { value } = await this.api.post({
631
- method: 'getAccountInfo',
632
- params: [address, { encoding: 'jsonParsed', commitment: 'single' }],
633
- })
650
+ const { value } = await this.rpcCall(
651
+ 'getAccountInfo',
652
+ [address, { encoding: 'jsonParsed', commitment: 'single' }],
653
+ { address }
654
+ )
634
655
  return value
635
656
  }
636
657
 
@@ -649,8 +670,8 @@ export class Api {
649
670
  }
650
671
 
651
672
  async getDecimals(tokenMintAddress: string) {
652
- const res = await this.api.post({ method: 'getTokenSupply', params: [tokenMintAddress] })
653
- return lodash.get(res, 'value.decimals', null)
673
+ const result = await this.rpcCall('getTokenSupply', [tokenMintAddress])
674
+ return lodash.get(result, 'value.decimals', null)
654
675
  }
655
676
 
656
677
  async getAddressType(address: string) {
@@ -694,24 +715,22 @@ export class Api {
694
715
  }
695
716
 
696
717
  async getStakeAccountsInfo(address: string) {
697
- // get staked amount and other info
698
- const res = await this.api.post({
699
- method: 'getProgramAccounts',
700
- params: [
701
- STAKE_PROGRAM_ID.toBase58(),
702
- {
703
- filters: [
704
- {
705
- memcmp: {
706
- offset: 12,
707
- bytes: address,
708
- },
718
+ const params = [
719
+ STAKE_PROGRAM_ID.toBase58(),
720
+ {
721
+ filters: [
722
+ {
723
+ memcmp: {
724
+ offset: 12,
725
+ bytes: address,
709
726
  },
710
- ],
711
- encoding: 'jsonParsed',
712
- },
713
- ],
714
- })
727
+ },
728
+ ],
729
+ encoding: 'jsonParsed',
730
+ },
731
+ ]
732
+ const res = await this.rpcCall('getProgramAccounts', params, { address })
733
+
715
734
  const accounts = {}
716
735
  let totalStake = 0
717
736
  let locked = 0
@@ -760,12 +779,16 @@ export class Api {
760
779
  }
761
780
 
762
781
  async getMinimumBalanceForRentExemption(size: number) {
763
- const minimumBalance = await this.api.post({
764
- method: 'getMinimumBalanceForRentExemption',
765
- params: [size],
766
- })
782
+ return this.rpcCall('getMinimumBalanceForRentExemption', [size])
783
+ }
784
+
785
+ async getProgramAccounts(programId: string, config) {
786
+ return this.rpcCall('getProgramAccounts', [programId, config])
787
+ }
767
788
 
768
- return minimumBalance
789
+ async getMultipleAccounts(pubkeys: string[], config) {
790
+ const response = await this.rpcCall('getMultipleAccounts', [pubkeys, config])
791
+ return response && response.value ? response.value : []
769
792
  }
770
793
 
771
794
  /**
@@ -774,10 +797,8 @@ export class Api {
774
797
  broadcastTransaction = async (signedTx: string): string => {
775
798
  console.log('Solana broadcasting TX:', signedTx) // base64
776
799
 
777
- const result = await this.api.post({
778
- method: 'sendTransaction',
779
- params: [signedTx, { encoding: 'base64', commitment: 'singleGossip' }],
780
- })
800
+ const params = [signedTx, { encoding: 'base64', commitment: 'finalized' }]
801
+ const result = await this.rpcCall('sendTransaction', params, { forceHttp: true })
781
802
 
782
803
  console.log(`tx ${JSON.stringify(result)} sent!`)
783
804
  return result || null
@@ -786,10 +807,7 @@ export class Api {
786
807
  simulateTransaction = async (encodedTransaction, options) => {
787
808
  const {
788
809
  value: { accounts },
789
- } = await this.api.post({
790
- method: 'simulateTransaction',
791
- params: [encodedTransaction, options],
792
- })
810
+ } = await this.rpcCall('simulateTransaction', [encodedTransaction, options])
793
811
 
794
812
  return accounts
795
813
  }
@@ -0,0 +1,237 @@
1
+ import ms from 'ms'
2
+ import delay from 'delay'
3
+ import url from 'url'
4
+ import lodash from 'lodash'
5
+ import debugLogger from 'debug'
6
+ import { WebSocket } from '@exodus/fetch'
7
+
8
+ // WS subscriptions: https://docs.solana.com/developing/clients/jsonrpc-api#subscription-websocket
9
+
10
+ const SOLANA_DEFAULT_ENDPOINT = 'wss://solana.a.exodus.io/ws'
11
+ const DEFAULT_RECONNECT_DELAY = ms('15s')
12
+ const PING_INTERVAL = ms('50s')
13
+ const TIMEOUT = ms('20s')
14
+
15
+ const debug = debugLogger('exodus:solana-api')
16
+
17
+ export class Connection {
18
+ constructor({
19
+ endpoint = SOLANA_DEFAULT_ENDPOINT,
20
+ address,
21
+ tokensAddresses = [],
22
+ callback,
23
+ reconnectCallback = () => {},
24
+ reconnectDelay = DEFAULT_RECONNECT_DELAY,
25
+ }) {
26
+ this.address = address
27
+ this.tokensAddresses = tokensAddresses
28
+ this.endpoint = endpoint
29
+ this.callback = callback
30
+ this.reconnectCallback = reconnectCallback
31
+ this.reconnectDelay = reconnectDelay
32
+
33
+ this.shutdown = false
34
+ this.ws = null
35
+ this.rpcQueue = {}
36
+ this.messageQueue = []
37
+ this.inProcessMessages = false
38
+ this.pingTimeout = null
39
+ this.reconnectTimeout = null
40
+ this.txCache = {}
41
+ }
42
+
43
+ newSocket(reqUrl) {
44
+ // eslint-disable-next-line
45
+ const obj = url.parse(reqUrl)
46
+ obj.protocol = 'wss:'
47
+ reqUrl = url.format(obj)
48
+ debug('Opening WS to:', reqUrl)
49
+ const ws = new WebSocket(`${reqUrl}`)
50
+ ws.onmessage = this.onMessage.bind(this)
51
+ ws.onopen = this.onOpen.bind(this)
52
+ ws.onclose = this.onClose.bind(this)
53
+ ws.onerror = this.onError.bind(this)
54
+ return ws
55
+ }
56
+
57
+ get isConnecting() {
58
+ return !!(this.ws && this.ws.readyState === WebSocket.CONNECTING)
59
+ }
60
+
61
+ get isOpen() {
62
+ return !!(this.ws && this.ws.readyState === WebSocket.OPEN)
63
+ }
64
+
65
+ get isClosing() {
66
+ return !!(this.ws && this.ws.readyState === WebSocket.CLOSING)
67
+ }
68
+
69
+ get isClosed() {
70
+ return !!(!this.ws || this.ws.readyState === WebSocket.CLOSED)
71
+ }
72
+
73
+ get running() {
74
+ return !!(!this.isClosed || this.inProcessMessages || this.messageQueue.length)
75
+ }
76
+
77
+ get connectionState() {
78
+ if (this.isConnecting) return 'CONNECTING'
79
+ else if (this.isOpen) return 'OPEN'
80
+ else if (this.isClosing) return 'CLOSING'
81
+ else if (this.isClosed) return 'CLOSED'
82
+ return 'NONE'
83
+ }
84
+
85
+ doPing() {
86
+ if (this.ws) {
87
+ this.ws.ping()
88
+ this.pingTimeout = setTimeout(this.doPing.bind(this), PING_INTERVAL)
89
+ }
90
+ }
91
+
92
+ doRestart() {
93
+ // debug('Restarting WS:')
94
+ this.reconnectTimeout = setTimeout(async () => {
95
+ try {
96
+ debug('reconnecting ws...')
97
+ this.start()
98
+ await this.reconnectCallback()
99
+ } catch (e) {
100
+ console.log(`Error in reconnect callback: ${e.message}`)
101
+ }
102
+ }, this.reconnectDelay)
103
+ }
104
+
105
+ onMessage(evt) {
106
+ try {
107
+ const json = JSON.parse(evt.data)
108
+ debug('new ws msg:', json)
109
+ if (!json.error) {
110
+ if (lodash.get(this.rpcQueue, json.id)) {
111
+ // json-rpc reply
112
+ clearTimeout(this.rpcQueue[json.id].timeout)
113
+ this.rpcQueue[json.id].resolve(json.result)
114
+ delete this.rpcQueue[json.id]
115
+ } else if (json.method) {
116
+ const msg = { method: json.method, ...lodash.get(json, 'params.result', json.result) }
117
+ debug('pushing msg to queue', msg)
118
+ this.messageQueue.push(msg) // sub results
119
+ }
120
+ this.processMessages()
121
+ } else {
122
+ if (lodash.get(this.rpcQueue, json.id)) {
123
+ this.rpcQueue[json.id].reject(new Error(json.error.message))
124
+ clearTimeout(this.rpcQueue[json.id].timeout)
125
+ delete this.rpcQueue[json.id]
126
+ } else debug('Unsupported WS message:', json.error.message)
127
+ }
128
+ } catch (e) {
129
+ debug(e)
130
+ debug('Cannot parse msg:', evt.data)
131
+ }
132
+ }
133
+
134
+ onOpen(evt) {
135
+ debug('Opened WS')
136
+ // subscribe to each addresses (SOL and ASA addr)
137
+
138
+ this.tokensAddresses.concat(this.address).forEach((address) => {
139
+ // sub for account state changes
140
+ this.ws.send(
141
+ JSON.stringify({
142
+ jsonrpc: '2.0',
143
+ method: 'accountSubscribe',
144
+ params: [
145
+ address,
146
+ {
147
+ encoding: 'jsonParsed',
148
+ },
149
+ ],
150
+ id: 1,
151
+ })
152
+ )
153
+ // sub for incoming/outcoming txs
154
+ this.ws.send(
155
+ JSON.stringify({
156
+ jsonrpc: '2.0',
157
+ method: 'logsSubscribe',
158
+ params: [{ mentions: [address] }, { commitment: 'finalized' }],
159
+ id: 2,
160
+ })
161
+ )
162
+ })
163
+ // this.doPing()
164
+ }
165
+
166
+ onError(evt) {
167
+ debug('Error on WS:', evt.data)
168
+ }
169
+
170
+ onClose(evt) {
171
+ debug('Closing WS')
172
+ clearTimeout(this.pingTimeout)
173
+ clearTimeout(this.reconnectTimeout)
174
+ if (!this.shutdown) {
175
+ this.doRestart()
176
+ }
177
+ }
178
+
179
+ async sendMessage(method, params = []) {
180
+ return new Promise((resolve, reject) => {
181
+ if (this.isClosed || this.shutdown) return reject(new Error('connection not started'))
182
+ const id = Math.floor(Math.random() * 1e7) + 1
183
+
184
+ this.rpcQueue[id] = { resolve, reject }
185
+ this.rpcQueue[id].timeout = setTimeout(() => {
186
+ delete this.rpcQueue[id]
187
+ reject(new Error('solana ws: reply timeout'))
188
+ }, TIMEOUT)
189
+ this.ws.send(JSON.stringify({ jsonrpc: '2.0', method, params, id }))
190
+ })
191
+ }
192
+
193
+ async processMessages() {
194
+ if (this.inProcessMessages) return null
195
+ this.inProcessMessages = true
196
+ try {
197
+ while (this.messageQueue.length) {
198
+ const items = this.messageQueue.splice(0, this.messageQueue.length)
199
+ await this.callback(items)
200
+ }
201
+ } catch (e) {
202
+ console.log(`Solana: error processing streams: ${e.message}`)
203
+ } finally {
204
+ this.inProcessMessages = false
205
+ }
206
+ }
207
+
208
+ async close() {
209
+ clearTimeout(this.reconnectTimeout)
210
+ clearTimeout(this.pingTimeout)
211
+ if (this.ws && (this.isConnecting || this.isOpen)) {
212
+ // this.ws.send(JSON.stringify({ method: 'close' }))
213
+ await delay(ms('1s')) // allow for the 'close' round-trip
214
+ await this.ws.close()
215
+ await this.ws.terminate()
216
+ }
217
+ }
218
+
219
+ async start() {
220
+ try {
221
+ if (!this.isClosed || this.shutdown) return
222
+ this.ws = this.newSocket(this.endpoint)
223
+ } catch (e) {
224
+ console.log('Solana: error starting WS:', e)
225
+ this.doRestart()
226
+ }
227
+ }
228
+
229
+ async stop() {
230
+ if (this.shutdown) return
231
+ this.shutdown = true
232
+ await this.close()
233
+ while (this.running) {
234
+ await delay(ms('1s'))
235
+ }
236
+ }
237
+ } // Connection
@@ -4,7 +4,11 @@ import assert from 'minimalistic-assert'
4
4
 
5
5
  const DEFAULT_POOL_ADDRESS = '9QU2QSxhb24FUX3Tu2FpczXjpK3VYrvRudywSZaM29mF' // Everstake
6
6
 
7
- const DEFAULT_REMOTE_CONFIG = { rpcs: [], staking: { enabled: true, pool: DEFAULT_POOL_ADDRESS } }
7
+ const DEFAULT_REMOTE_CONFIG = {
8
+ rpcs: [],
9
+ ws: [],
10
+ staking: { enabled: true, pool: DEFAULT_POOL_ADDRESS },
11
+ }
8
12
 
9
13
  export class SolanaMonitor extends BaseMonitor {
10
14
  constructor({ api, includeUnparsed = false, ...args }) {
@@ -14,11 +18,45 @@ export class SolanaMonitor extends BaseMonitor {
14
18
  this.cursors = {}
15
19
  this.assets = {}
16
20
  this.includeUnparsed = includeUnparsed
21
+ this.addHook('before-stop', (...args) => this.beforeStop(...args))
22
+ }
23
+
24
+ async beforeStop() {
25
+ const walletAccounts = await this.aci.getWalletAccounts({ assetName: this.asset.name })
26
+ return Promise.all(walletAccounts.map((walletAccount) => this.stopListener({ walletAccount })))
27
+ }
28
+
29
+ async initWalletAccount({ walletAccount }) {
30
+ if (this.tickCount[walletAccount] === 0) {
31
+ await this.startListener({ walletAccount })
32
+ }
33
+ }
34
+
35
+ async startListener({ walletAccount }) {
36
+ const address = await this.aci.getReceiveAddress({ assetName: this.asset.name, walletAccount })
37
+ return this.api.watchAddress({
38
+ address,
39
+ /*
40
+ // OPTIONAL. Relying on polling through ws
41
+ tokensAddresses: [], // needed for ASA subs
42
+ handleAccounts: (updates) => this.accountsCallback({ updates, walletAccount }),
43
+ handleTransfers: (txs) => {
44
+ // new SOL tx, ticking monitor
45
+ this.tick({ walletAccount }) // it will cause refresh for both sender/receiver. Without necessarily fetching the tx if it's not finalized in the node.
46
+ },
47
+ */
48
+ })
49
+ }
50
+
51
+ async stopListener({ walletAccount }) {
52
+ const address = await this.aci.getReceiveAddress({ assetName: this.asset.name, walletAccount })
53
+ return this.api.unwatchAddress({ address })
17
54
  }
18
55
 
19
56
  setServer(config = {}) {
20
- const { rpcs, staking = {} } = { ...DEFAULT_REMOTE_CONFIG, ...config }
57
+ const { rpcs, ws, staking = {} } = { ...DEFAULT_REMOTE_CONFIG, ...config }
21
58
  this.api.setServer(rpcs[0])
59
+ this.api.setWsEndpoint(ws[0])
22
60
  this.staking = staking
23
61
  }
24
62
 
@@ -35,6 +73,9 @@ export class SolanaMonitor extends BaseMonitor {
35
73
  }
36
74
 
37
75
  async tick({ walletAccount, refresh }) {
76
+ // Check for new wallet account
77
+ await this.initWalletAccount({ walletAccount })
78
+
38
79
  const assetName = this.asset.name
39
80
  this.assets = await this.aci.getAssetsForNetwork({ baseAssetName: assetName })
40
81
  this.api.setTokens(this.assets)