@exodus/solana-api 2.0.12 → 2.0.14

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.12",
3
+ "version": "2.0.14",
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",
17
+ "@exodus/asset-lib": "^3.6.2",
18
18
  "@exodus/assets": "^8.0.71",
19
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": "41f7a0666330e4ab63c7d38247bbf2fb7f4267fb"
33
+ "gitHead": "16410d60f413eda6aeb3372c23875b98c20de06c"
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,103 @@ 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('getTransaction', [
130
+ id,
131
+ { encoding: 'jsonParsed', maxSupportedTransactionVersion: 0 },
132
+ ])
87
133
  }
88
134
 
89
135
  async getFee(): number {
90
- const {
91
- value: {
92
- feeCalculator: { lamportsPerSignature },
93
- },
94
- } = await this.api.post({
95
- method: 'getRecentBlockhash',
96
- })
97
- return lamportsPerSignature
136
+ const result = await this.rpcCall('getRecentBlockhash', [
137
+ { commitment: 'finalized', encoding: 'jsonParsed' },
138
+ ])
139
+ return lodash.get(result, 'value.feeCalculator.lamportsPerSignature')
98
140
  }
99
141
 
100
142
  async getBalance(address: string): number {
101
- const res = await this.api.post({
102
- method: 'getBalance',
103
- params: [address],
143
+ const result = await this.rpcCall('getBalance', [address, { encoding: 'jsonParsed' }], {
144
+ address,
104
145
  })
105
- return res.value || 0
146
+ return lodash.get(result, 'value', 0)
106
147
  }
107
148
 
108
149
  async getBlockTime(slot: number) {
109
150
  // 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
- })
151
+ return this.rpcCall('getBlockTime', [slot])
114
152
  }
115
153
 
116
154
  async getConfirmedSignaturesForAddress(address: string, { until, before, limit } = {}): any {
117
155
  until = until || undefined
118
- return this.api.post({
119
- method: 'getSignaturesForAddress',
120
- params: [address, { until, before, limit }],
121
- })
156
+ return this.rpcCall('getSignaturesForAddress', [address, { until, before, limit }], { address })
122
157
  }
123
158
 
124
159
  /**
@@ -221,7 +256,8 @@ export class Api {
221
256
 
222
257
  return {
223
258
  unparsed: true,
224
- amount: postBalances[ownerIndex] - preBalances[ownerIndex] + feePaid,
259
+ amount:
260
+ ownerIndex === -1 ? 0 : postBalances[ownerIndex] - preBalances[ownerIndex] + feePaid,
225
261
  fee: feePaid,
226
262
  data: {
227
263
  meta: txDetails.meta,
@@ -524,13 +560,8 @@ export class Api {
524
560
  }
525
561
 
526
562
  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
563
+ const result = await this.rpcCall('getTokenSupply', [mintAddress])
564
+ return lodash.get(result, 'value.amount')
534
565
  }
535
566
 
536
567
  async getWalletTokensList({ tokenAccounts }) {
@@ -556,10 +587,11 @@ export class Api {
556
587
  }
557
588
 
558
589
  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
- })
590
+ const { value: accountsList } = await this.rpcCall(
591
+ 'getTokenAccountsByOwner',
592
+ [address, { programId: TOKEN_PROGRAM_ID.toBase58() }, { encoding: 'jsonParsed' }],
593
+ { address }
594
+ )
563
595
 
564
596
  const tokenAccounts = []
565
597
  for (let entry of accountsList) {
@@ -604,10 +636,7 @@ export class Api {
604
636
  async isAssociatedTokenAccountActive(tokenAddress: string) {
605
637
  // Returns the token balance of an SPL Token account.
606
638
  try {
607
- await this.api.post({
608
- method: 'getTokenAccountBalance',
609
- params: [tokenAddress],
610
- })
639
+ await this.rpcCall('getTokenAccountBalance', [tokenAddress])
611
640
  return true
612
641
  } catch (e) {
613
642
  return false
@@ -616,21 +645,16 @@ export class Api {
616
645
 
617
646
  // Returns account balance of a SPL Token account.
618
647
  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
648
+ const result = await this.rpcCall('getTokenAccountBalance', [tokenAddress])
649
+ return lodash.get(result, 'value.amount')
627
650
  }
628
651
 
629
652
  async getAccountInfo(address: string) {
630
- const { value } = await this.api.post({
631
- method: 'getAccountInfo',
632
- params: [address, { encoding: 'jsonParsed', commitment: 'single' }],
633
- })
653
+ const { value } = await this.rpcCall(
654
+ 'getAccountInfo',
655
+ [address, { encoding: 'jsonParsed', commitment: 'single' }],
656
+ { address }
657
+ )
634
658
  return value
635
659
  }
636
660
 
@@ -649,8 +673,8 @@ export class Api {
649
673
  }
650
674
 
651
675
  async getDecimals(tokenMintAddress: string) {
652
- const res = await this.api.post({ method: 'getTokenSupply', params: [tokenMintAddress] })
653
- return lodash.get(res, 'value.decimals', null)
676
+ const result = await this.rpcCall('getTokenSupply', [tokenMintAddress])
677
+ return lodash.get(result, 'value.decimals', null)
654
678
  }
655
679
 
656
680
  async getAddressType(address: string) {
@@ -694,24 +718,22 @@ export class Api {
694
718
  }
695
719
 
696
720
  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
- },
721
+ const params = [
722
+ STAKE_PROGRAM_ID.toBase58(),
723
+ {
724
+ filters: [
725
+ {
726
+ memcmp: {
727
+ offset: 12,
728
+ bytes: address,
709
729
  },
710
- ],
711
- encoding: 'jsonParsed',
712
- },
713
- ],
714
- })
730
+ },
731
+ ],
732
+ encoding: 'jsonParsed',
733
+ },
734
+ ]
735
+ const res = await this.rpcCall('getProgramAccounts', params, { address })
736
+
715
737
  const accounts = {}
716
738
  let totalStake = 0
717
739
  let locked = 0
@@ -760,29 +782,15 @@ export class Api {
760
782
  }
761
783
 
762
784
  async getMinimumBalanceForRentExemption(size: number) {
763
- const minimumBalance = await this.api.post({
764
- method: 'getMinimumBalanceForRentExemption',
765
- params: [size],
766
- })
767
-
768
- return minimumBalance
785
+ return this.rpcCall('getMinimumBalanceForRentExemption', [size])
769
786
  }
770
787
 
771
788
  async getProgramAccounts(programId: string, config) {
772
- const response = await this.api.post({
773
- method: 'getProgramAccounts',
774
- params: [programId, config],
775
- })
776
-
777
- return response
789
+ return this.rpcCall('getProgramAccounts', [programId, config])
778
790
  }
779
791
 
780
792
  async getMultipleAccounts(pubkeys: string[], config) {
781
- const response = await this.api.post({
782
- method: 'getMultipleAccounts',
783
- params: [pubkeys, config],
784
- })
785
-
793
+ const response = await this.rpcCall('getMultipleAccounts', [pubkeys, config])
786
794
  return response && response.value ? response.value : []
787
795
  }
788
796
 
@@ -792,10 +800,8 @@ export class Api {
792
800
  broadcastTransaction = async (signedTx: string): string => {
793
801
  console.log('Solana broadcasting TX:', signedTx) // base64
794
802
 
795
- const result = await this.api.post({
796
- method: 'sendTransaction',
797
- params: [signedTx, { encoding: 'base64', commitment: 'singleGossip' }],
798
- })
803
+ const params = [signedTx, { encoding: 'base64', commitment: 'finalized' }]
804
+ const result = await this.rpcCall('sendTransaction', params, { forceHttp: true })
799
805
 
800
806
  console.log(`tx ${JSON.stringify(result)} sent!`)
801
807
  return result || null
@@ -804,10 +810,7 @@ export class Api {
804
810
  simulateTransaction = async (encodedTransaction, options) => {
805
811
  const {
806
812
  value: { accounts },
807
- } = await this.api.post({
808
- method: 'simulateTransaction',
809
- params: [encodedTransaction, options],
810
- })
813
+ } = await this.rpcCall('simulateTransaction', [encodedTransaction, options])
811
814
 
812
815
  return accounts
813
816
  }
@@ -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)