@exodus/solana-api 2.0.14 → 2.0.16
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 +4 -6
- package/src/api.js +110 -112
- package/src/tx-log/solana-monitor.js +2 -43
- package/src/connection.js +0 -237
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/solana-api",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.16",
|
|
4
4
|
"description": "Exodus internal Solana asset API wrapper",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"files": [
|
|
@@ -14,15 +14,13 @@
|
|
|
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.0",
|
|
18
18
|
"@exodus/assets": "^8.0.71",
|
|
19
19
|
"@exodus/assets-base": "^8.0.150",
|
|
20
|
-
"@exodus/fetch": "^1.2.0",
|
|
21
20
|
"@exodus/models": "^8.7.2",
|
|
22
21
|
"@exodus/nfts-core": "^0.5.0",
|
|
23
|
-
"@exodus/solana-lib": "^1.3.
|
|
22
|
+
"@exodus/solana-lib": "^1.3.14",
|
|
24
23
|
"bn.js": "^4.11.0",
|
|
25
|
-
"debug": "^4.1.1",
|
|
26
24
|
"lodash": "^4.17.11",
|
|
27
25
|
"url-join": "4.0.0",
|
|
28
26
|
"wretch": "^1.5.2"
|
|
@@ -30,5 +28,5 @@
|
|
|
30
28
|
"devDependencies": {
|
|
31
29
|
"node-fetch": "~2.6.0"
|
|
32
30
|
},
|
|
33
|
-
"gitHead": "
|
|
31
|
+
"gitHead": "9ec7084bcdfe14f1c6f11df393351885773046a6"
|
|
34
32
|
}
|
package/src/api.js
CHANGED
|
@@ -20,21 +20,17 @@ 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'
|
|
24
23
|
|
|
25
24
|
// Doc: https://docs.solana.com/apps/jsonrpc-api
|
|
26
25
|
|
|
27
26
|
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'
|
|
29
27
|
|
|
30
28
|
// Tokens + SOL api support
|
|
31
29
|
export class Api {
|
|
32
|
-
constructor(rpcUrl
|
|
30
|
+
constructor(rpcUrl) {
|
|
33
31
|
this.setServer(rpcUrl)
|
|
34
|
-
this.setWsEndpoint(wsUrl)
|
|
35
32
|
this.setTokens(assets)
|
|
36
33
|
this.tokensToSkip = {}
|
|
37
|
-
this.connections = {}
|
|
38
34
|
}
|
|
39
35
|
|
|
40
36
|
setServer(rpcUrl) {
|
|
@@ -42,10 +38,6 @@ export class Api {
|
|
|
42
38
|
this.api = createApi(this.rpcUrl)
|
|
43
39
|
}
|
|
44
40
|
|
|
45
|
-
setWsEndpoint(wsUrl) {
|
|
46
|
-
this.wsUrl = wsUrl || WS_ENDPOINT
|
|
47
|
-
}
|
|
48
|
-
|
|
49
41
|
setTokens(assets = {}) {
|
|
50
42
|
const solTokens = lodash.pickBy(assets, (asset) => asset.assetType === 'SOLANA_TOKEN')
|
|
51
43
|
this.tokens = lodash.mapKeys(solTokens, (v) => v.mintAddress)
|
|
@@ -57,103 +49,76 @@ export class Api {
|
|
|
57
49
|
})
|
|
58
50
|
}
|
|
59
51
|
|
|
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
|
-
|
|
104
52
|
isTokenSupported(mint: string) {
|
|
105
53
|
return !!this.tokens[mint]
|
|
106
54
|
}
|
|
107
55
|
|
|
108
56
|
async getEpochInfo(): number {
|
|
109
|
-
const { epoch } = await this.
|
|
57
|
+
const { epoch } = await this.api.post({
|
|
58
|
+
method: 'getEpochInfo',
|
|
59
|
+
})
|
|
110
60
|
return Number(epoch)
|
|
111
61
|
}
|
|
112
62
|
|
|
113
63
|
async getStakeActivation(address): string {
|
|
114
|
-
const { state } = await this.
|
|
64
|
+
const { state } = await this.api.post({
|
|
65
|
+
method: 'getStakeActivation',
|
|
66
|
+
params: [address],
|
|
67
|
+
})
|
|
115
68
|
return state
|
|
116
69
|
}
|
|
117
70
|
|
|
118
71
|
async getRecentBlockHash(): string {
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
)
|
|
124
|
-
return
|
|
72
|
+
const {
|
|
73
|
+
value: { blockhash },
|
|
74
|
+
} = await this.api.post({
|
|
75
|
+
method: 'getRecentBlockhash',
|
|
76
|
+
})
|
|
77
|
+
return blockhash
|
|
125
78
|
}
|
|
126
79
|
|
|
127
80
|
// Transaction structure: https://docs.solana.com/apps/jsonrpc-api#transaction-structure
|
|
128
81
|
async getTransactionById(id: string) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
{ encoding: 'jsonParsed', maxSupportedTransactionVersion: 0 },
|
|
132
|
-
|
|
82
|
+
const result = await this.api.post({
|
|
83
|
+
method: 'getTransaction',
|
|
84
|
+
params: [id, { encoding: 'jsonParsed', maxSupportedTransactionVersion: 0 }],
|
|
85
|
+
})
|
|
86
|
+
return result
|
|
133
87
|
}
|
|
134
88
|
|
|
135
89
|
async getFee(): number {
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
90
|
+
const {
|
|
91
|
+
value: {
|
|
92
|
+
feeCalculator: { lamportsPerSignature },
|
|
93
|
+
},
|
|
94
|
+
} = await this.api.post({
|
|
95
|
+
method: 'getRecentBlockhash',
|
|
96
|
+
})
|
|
97
|
+
return lamportsPerSignature
|
|
140
98
|
}
|
|
141
99
|
|
|
142
100
|
async getBalance(address: string): number {
|
|
143
|
-
const
|
|
144
|
-
|
|
101
|
+
const res = await this.api.post({
|
|
102
|
+
method: 'getBalance',
|
|
103
|
+
params: [address],
|
|
145
104
|
})
|
|
146
|
-
return
|
|
105
|
+
return res.value || 0
|
|
147
106
|
}
|
|
148
107
|
|
|
149
108
|
async getBlockTime(slot: number) {
|
|
150
109
|
// might result in error if executed on a validator with partial ledger (https://github.com/solana-labs/solana/issues/12413)
|
|
151
|
-
return this.
|
|
110
|
+
return this.api.post({
|
|
111
|
+
method: 'getBlockTime',
|
|
112
|
+
params: [slot],
|
|
113
|
+
})
|
|
152
114
|
}
|
|
153
115
|
|
|
154
116
|
async getConfirmedSignaturesForAddress(address: string, { until, before, limit } = {}): any {
|
|
155
117
|
until = until || undefined
|
|
156
|
-
return this.
|
|
118
|
+
return this.api.post({
|
|
119
|
+
method: 'getSignaturesForAddress',
|
|
120
|
+
params: [address, { until, before, limit }],
|
|
121
|
+
})
|
|
157
122
|
}
|
|
158
123
|
|
|
159
124
|
/**
|
|
@@ -560,8 +525,13 @@ export class Api {
|
|
|
560
525
|
}
|
|
561
526
|
|
|
562
527
|
async getSupply(mintAddress: string): string {
|
|
563
|
-
const
|
|
564
|
-
|
|
528
|
+
const {
|
|
529
|
+
value: { amount },
|
|
530
|
+
} = await this.api.post({
|
|
531
|
+
method: 'getTokenSupply',
|
|
532
|
+
params: [mintAddress],
|
|
533
|
+
})
|
|
534
|
+
return amount
|
|
565
535
|
}
|
|
566
536
|
|
|
567
537
|
async getWalletTokensList({ tokenAccounts }) {
|
|
@@ -587,11 +557,10 @@ export class Api {
|
|
|
587
557
|
}
|
|
588
558
|
|
|
589
559
|
async getTokenAccountsByOwner(address: string, tokenTicker: ?string): Array {
|
|
590
|
-
const { value: accountsList } = await this.
|
|
591
|
-
'getTokenAccountsByOwner',
|
|
592
|
-
[address, { programId: TOKEN_PROGRAM_ID.toBase58() }, { encoding: 'jsonParsed' }],
|
|
593
|
-
|
|
594
|
-
)
|
|
560
|
+
const { value: accountsList } = await this.api.post({
|
|
561
|
+
method: 'getTokenAccountsByOwner',
|
|
562
|
+
params: [address, { programId: TOKEN_PROGRAM_ID.toBase58() }, { encoding: 'jsonParsed' }],
|
|
563
|
+
})
|
|
595
564
|
|
|
596
565
|
const tokenAccounts = []
|
|
597
566
|
for (let entry of accountsList) {
|
|
@@ -636,7 +605,10 @@ export class Api {
|
|
|
636
605
|
async isAssociatedTokenAccountActive(tokenAddress: string) {
|
|
637
606
|
// Returns the token balance of an SPL Token account.
|
|
638
607
|
try {
|
|
639
|
-
await this.
|
|
608
|
+
await this.api.post({
|
|
609
|
+
method: 'getTokenAccountBalance',
|
|
610
|
+
params: [tokenAddress],
|
|
611
|
+
})
|
|
640
612
|
return true
|
|
641
613
|
} catch (e) {
|
|
642
614
|
return false
|
|
@@ -645,16 +617,21 @@ export class Api {
|
|
|
645
617
|
|
|
646
618
|
// Returns account balance of a SPL Token account.
|
|
647
619
|
async getTokenBalance(tokenAddress: string) {
|
|
648
|
-
const
|
|
649
|
-
|
|
620
|
+
const {
|
|
621
|
+
value: { amount },
|
|
622
|
+
} = await this.api.post({
|
|
623
|
+
method: 'getTokenAccountBalance',
|
|
624
|
+
params: [tokenAddress],
|
|
625
|
+
})
|
|
626
|
+
|
|
627
|
+
return amount
|
|
650
628
|
}
|
|
651
629
|
|
|
652
630
|
async getAccountInfo(address: string) {
|
|
653
|
-
const { value } = await this.
|
|
654
|
-
'getAccountInfo',
|
|
655
|
-
[address, { encoding: 'jsonParsed', commitment: 'single' }],
|
|
656
|
-
|
|
657
|
-
)
|
|
631
|
+
const { value } = await this.api.post({
|
|
632
|
+
method: 'getAccountInfo',
|
|
633
|
+
params: [address, { encoding: 'jsonParsed', commitment: 'single' }],
|
|
634
|
+
})
|
|
658
635
|
return value
|
|
659
636
|
}
|
|
660
637
|
|
|
@@ -673,8 +650,8 @@ export class Api {
|
|
|
673
650
|
}
|
|
674
651
|
|
|
675
652
|
async getDecimals(tokenMintAddress: string) {
|
|
676
|
-
const
|
|
677
|
-
return lodash.get(
|
|
653
|
+
const res = await this.api.post({ method: 'getTokenSupply', params: [tokenMintAddress] })
|
|
654
|
+
return lodash.get(res, 'value.decimals', null)
|
|
678
655
|
}
|
|
679
656
|
|
|
680
657
|
async getAddressType(address: string) {
|
|
@@ -718,22 +695,24 @@ export class Api {
|
|
|
718
695
|
}
|
|
719
696
|
|
|
720
697
|
async getStakeAccountsInfo(address: string) {
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
698
|
+
// get staked amount and other info
|
|
699
|
+
const res = await this.api.post({
|
|
700
|
+
method: 'getProgramAccounts',
|
|
701
|
+
params: [
|
|
702
|
+
STAKE_PROGRAM_ID.toBase58(),
|
|
703
|
+
{
|
|
704
|
+
filters: [
|
|
705
|
+
{
|
|
706
|
+
memcmp: {
|
|
707
|
+
offset: 12,
|
|
708
|
+
bytes: address,
|
|
709
|
+
},
|
|
729
710
|
},
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
const res = await this.rpcCall('getProgramAccounts', params, { address })
|
|
736
|
-
|
|
711
|
+
],
|
|
712
|
+
encoding: 'jsonParsed',
|
|
713
|
+
},
|
|
714
|
+
],
|
|
715
|
+
})
|
|
737
716
|
const accounts = {}
|
|
738
717
|
let totalStake = 0
|
|
739
718
|
let locked = 0
|
|
@@ -782,15 +761,29 @@ export class Api {
|
|
|
782
761
|
}
|
|
783
762
|
|
|
784
763
|
async getMinimumBalanceForRentExemption(size: number) {
|
|
785
|
-
|
|
764
|
+
const minimumBalance = await this.api.post({
|
|
765
|
+
method: 'getMinimumBalanceForRentExemption',
|
|
766
|
+
params: [size],
|
|
767
|
+
})
|
|
768
|
+
|
|
769
|
+
return minimumBalance
|
|
786
770
|
}
|
|
787
771
|
|
|
788
772
|
async getProgramAccounts(programId: string, config) {
|
|
789
|
-
|
|
773
|
+
const response = await this.api.post({
|
|
774
|
+
method: 'getProgramAccounts',
|
|
775
|
+
params: [programId, config],
|
|
776
|
+
})
|
|
777
|
+
|
|
778
|
+
return response
|
|
790
779
|
}
|
|
791
780
|
|
|
792
781
|
async getMultipleAccounts(pubkeys: string[], config) {
|
|
793
|
-
const response = await this.
|
|
782
|
+
const response = await this.api.post({
|
|
783
|
+
method: 'getMultipleAccounts',
|
|
784
|
+
params: [pubkeys, config],
|
|
785
|
+
})
|
|
786
|
+
|
|
794
787
|
return response && response.value ? response.value : []
|
|
795
788
|
}
|
|
796
789
|
|
|
@@ -800,8 +793,10 @@ export class Api {
|
|
|
800
793
|
broadcastTransaction = async (signedTx: string): string => {
|
|
801
794
|
console.log('Solana broadcasting TX:', signedTx) // base64
|
|
802
795
|
|
|
803
|
-
const
|
|
804
|
-
|
|
796
|
+
const result = await this.api.post({
|
|
797
|
+
method: 'sendTransaction',
|
|
798
|
+
params: [signedTx, { encoding: 'base64', commitment: 'singleGossip' }],
|
|
799
|
+
})
|
|
805
800
|
|
|
806
801
|
console.log(`tx ${JSON.stringify(result)} sent!`)
|
|
807
802
|
return result || null
|
|
@@ -810,7 +805,10 @@ export class Api {
|
|
|
810
805
|
simulateTransaction = async (encodedTransaction, options) => {
|
|
811
806
|
const {
|
|
812
807
|
value: { accounts },
|
|
813
|
-
} = await this.
|
|
808
|
+
} = await this.api.post({
|
|
809
|
+
method: 'simulateTransaction',
|
|
810
|
+
params: [encodedTransaction, options],
|
|
811
|
+
})
|
|
814
812
|
|
|
815
813
|
return accounts
|
|
816
814
|
}
|
|
@@ -4,11 +4,7 @@ import assert from 'minimalistic-assert'
|
|
|
4
4
|
|
|
5
5
|
const DEFAULT_POOL_ADDRESS = '9QU2QSxhb24FUX3Tu2FpczXjpK3VYrvRudywSZaM29mF' // Everstake
|
|
6
6
|
|
|
7
|
-
const DEFAULT_REMOTE_CONFIG = {
|
|
8
|
-
rpcs: [],
|
|
9
|
-
ws: [],
|
|
10
|
-
staking: { enabled: true, pool: DEFAULT_POOL_ADDRESS },
|
|
11
|
-
}
|
|
7
|
+
const DEFAULT_REMOTE_CONFIG = { rpcs: [], staking: { enabled: true, pool: DEFAULT_POOL_ADDRESS } }
|
|
12
8
|
|
|
13
9
|
export class SolanaMonitor extends BaseMonitor {
|
|
14
10
|
constructor({ api, includeUnparsed = false, ...args }) {
|
|
@@ -18,45 +14,11 @@ export class SolanaMonitor extends BaseMonitor {
|
|
|
18
14
|
this.cursors = {}
|
|
19
15
|
this.assets = {}
|
|
20
16
|
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 })
|
|
54
17
|
}
|
|
55
18
|
|
|
56
19
|
setServer(config = {}) {
|
|
57
|
-
const { rpcs,
|
|
20
|
+
const { rpcs, staking = {} } = { ...DEFAULT_REMOTE_CONFIG, ...config }
|
|
58
21
|
this.api.setServer(rpcs[0])
|
|
59
|
-
this.api.setWsEndpoint(ws[0])
|
|
60
22
|
this.staking = staking
|
|
61
23
|
}
|
|
62
24
|
|
|
@@ -73,9 +35,6 @@ export class SolanaMonitor extends BaseMonitor {
|
|
|
73
35
|
}
|
|
74
36
|
|
|
75
37
|
async tick({ walletAccount, refresh }) {
|
|
76
|
-
// Check for new wallet account
|
|
77
|
-
await this.initWalletAccount({ walletAccount })
|
|
78
|
-
|
|
79
38
|
const assetName = this.asset.name
|
|
80
39
|
this.assets = await this.aci.getAssetsForNetwork({ baseAssetName: assetName })
|
|
81
40
|
this.api.setTokens(this.assets)
|
package/src/connection.js
DELETED
|
@@ -1,237 +0,0 @@
|
|
|
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
|