@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 +6 -4
- package/src/api.js +114 -111
- package/src/connection.js +237 -0
- package/src/tx-log/solana-monitor.js +43 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/solana-api",
|
|
3
|
-
"version": "2.0.
|
|
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.
|
|
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.
|
|
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": "
|
|
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.
|
|
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.
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
92
|
-
|
|
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
|
|
102
|
-
|
|
103
|
-
params: [address],
|
|
143
|
+
const result = await this.rpcCall('getBalance', [address, { encoding: 'jsonParsed' }], {
|
|
144
|
+
address,
|
|
104
145
|
})
|
|
105
|
-
return
|
|
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.
|
|
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.
|
|
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:
|
|
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
|
-
|
|
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.
|
|
560
|
-
|
|
561
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
631
|
-
|
|
632
|
-
|
|
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
|
|
653
|
-
return lodash.get(
|
|
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
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
796
|
-
|
|
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.
|
|
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 = {
|
|
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)
|