@exodus/solana-api 2.5.0 → 2.5.3
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/lib/account-state.js +43 -0
- package/lib/api.js +1049 -0
- package/lib/connection.js +263 -0
- package/lib/index.js +68 -0
- package/lib/tx-log/index.js +18 -0
- package/lib/tx-log/solana-monitor.js +357 -0
- package/package.json +6 -5
- package/src/__tests__/api.test.js +273 -0
- package/src/__tests__/assets.js +7 -0
- package/src/__tests__/fixtures.js +3166 -0
- package/src/__tests__/index.test.js +7 -0
- package/src/__tests__/staking.test.js +85 -0
- package/src/__tests__/token.test.js +374 -0
- package/src/__tests__/ws.test.js +72 -0
- package/src/account-state.js +2 -5
- package/src/api.js +2 -3
- package/src/index.js +10 -1
- package/src/tx-log/__tests__/solana-monitor-api-mock.js +382 -0
- package/src/tx-log/__tests__/solana-monitor.integration.test.js +128 -0
- package/src/tx-log/__tests__/solana-monitor.test.js +135 -0
- package/src/tx-log/solana-monitor.js +5 -2
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.Connection = void 0;
|
|
7
|
+
|
|
8
|
+
var _ms = _interopRequireDefault(require("ms"));
|
|
9
|
+
|
|
10
|
+
var _delay = _interopRequireDefault(require("delay"));
|
|
11
|
+
|
|
12
|
+
var _url = _interopRequireDefault(require("url"));
|
|
13
|
+
|
|
14
|
+
var _lodash = _interopRequireDefault(require("lodash"));
|
|
15
|
+
|
|
16
|
+
var _debug = _interopRequireDefault(require("debug"));
|
|
17
|
+
|
|
18
|
+
var _fetch = require("@exodus/fetch");
|
|
19
|
+
|
|
20
|
+
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
21
|
+
|
|
22
|
+
const SOLANA_DEFAULT_ENDPOINT = 'wss://solana.a.exodus.io/ws';
|
|
23
|
+
const DEFAULT_RECONNECT_DELAY = (0, _ms.default)('15s');
|
|
24
|
+
const PING_INTERVAL = (0, _ms.default)('60s');
|
|
25
|
+
const TIMEOUT = (0, _ms.default)('50s');
|
|
26
|
+
const debug = (0, _debug.default)('exodus:solana-api');
|
|
27
|
+
|
|
28
|
+
class Connection {
|
|
29
|
+
constructor({
|
|
30
|
+
endpoint = SOLANA_DEFAULT_ENDPOINT,
|
|
31
|
+
address,
|
|
32
|
+
tokensAddresses = [],
|
|
33
|
+
callback,
|
|
34
|
+
reconnectCallback = () => {},
|
|
35
|
+
reconnectDelay = DEFAULT_RECONNECT_DELAY
|
|
36
|
+
}) {
|
|
37
|
+
this.address = address;
|
|
38
|
+
this.tokensAddresses = tokensAddresses;
|
|
39
|
+
this.endpoint = endpoint;
|
|
40
|
+
this.callback = callback;
|
|
41
|
+
this.reconnectCallback = reconnectCallback;
|
|
42
|
+
this.reconnectDelay = reconnectDelay;
|
|
43
|
+
this.shutdown = false;
|
|
44
|
+
this.ws = null;
|
|
45
|
+
this.rpcQueue = {};
|
|
46
|
+
this.messageQueue = [];
|
|
47
|
+
this.inProcessMessages = false;
|
|
48
|
+
this.pingTimeout = null;
|
|
49
|
+
this.reconnectTimeout = null;
|
|
50
|
+
this.txCache = {};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
newSocket(reqUrl) {
|
|
54
|
+
// eslint-disable-next-line
|
|
55
|
+
const obj = _url.default.parse(reqUrl);
|
|
56
|
+
|
|
57
|
+
obj.protocol = 'wss:';
|
|
58
|
+
reqUrl = _url.default.format(obj);
|
|
59
|
+
debug('Opening WS to:', reqUrl);
|
|
60
|
+
const ws = new _fetch.WebSocket(`${reqUrl}`);
|
|
61
|
+
ws.onmessage = this.onMessage.bind(this);
|
|
62
|
+
ws.onopen = this.onOpen.bind(this);
|
|
63
|
+
ws.onclose = this.onClose.bind(this);
|
|
64
|
+
ws.onerror = this.onError.bind(this);
|
|
65
|
+
return ws;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
get isConnecting() {
|
|
69
|
+
return !!(this.ws && this.ws.readyState === _fetch.WebSocket.CONNECTING);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
get isOpen() {
|
|
73
|
+
return !!(this.ws && this.ws.readyState === _fetch.WebSocket.OPEN);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
get isClosing() {
|
|
77
|
+
return !!(this.ws && this.ws.readyState === _fetch.WebSocket.CLOSING);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
get isClosed() {
|
|
81
|
+
return !!(!this.ws || this.ws.readyState === _fetch.WebSocket.CLOSED);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
get running() {
|
|
85
|
+
return !!(!this.isClosed || this.inProcessMessages || this.messageQueue.length);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
get connectionState() {
|
|
89
|
+
if (this.isConnecting) return 'CONNECTING';else if (this.isOpen) return 'OPEN';else if (this.isClosing) return 'CLOSING';else if (this.isClosed) return 'CLOSED';
|
|
90
|
+
return 'NONE';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
doPing() {
|
|
94
|
+
if (this.ws) {
|
|
95
|
+
this.ws.ping();
|
|
96
|
+
this.pingTimeout = setTimeout(this.doPing.bind(this), PING_INTERVAL);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
doRestart() {
|
|
101
|
+
// debug('Restarting WS:')
|
|
102
|
+
this.reconnectTimeout = setTimeout(async () => {
|
|
103
|
+
try {
|
|
104
|
+
debug('reconnecting ws...');
|
|
105
|
+
this.start();
|
|
106
|
+
await this.reconnectCallback();
|
|
107
|
+
} catch (e) {
|
|
108
|
+
console.log(`Error in reconnect callback: ${e.message}`);
|
|
109
|
+
}
|
|
110
|
+
}, this.reconnectDelay);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
onMessage(evt) {
|
|
114
|
+
try {
|
|
115
|
+
const json = JSON.parse(evt.data);
|
|
116
|
+
debug('new ws msg:', json);
|
|
117
|
+
|
|
118
|
+
if (!json.error) {
|
|
119
|
+
if (_lodash.default.get(this.rpcQueue, json.id)) {
|
|
120
|
+
// json-rpc reply
|
|
121
|
+
clearTimeout(this.rpcQueue[json.id].timeout);
|
|
122
|
+
this.rpcQueue[json.id].resolve(json.result);
|
|
123
|
+
delete this.rpcQueue[json.id];
|
|
124
|
+
} else if (json.method) {
|
|
125
|
+
const msg = {
|
|
126
|
+
method: json.method,
|
|
127
|
+
..._lodash.default.get(json, 'params.result', json.result)
|
|
128
|
+
};
|
|
129
|
+
debug('pushing msg to queue', msg);
|
|
130
|
+
this.messageQueue.push(msg); // sub results
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
this.processMessages();
|
|
134
|
+
} else {
|
|
135
|
+
if (_lodash.default.get(this.rpcQueue, json.id)) {
|
|
136
|
+
this.rpcQueue[json.id].reject(new Error(json.error.message));
|
|
137
|
+
clearTimeout(this.rpcQueue[json.id].timeout);
|
|
138
|
+
delete this.rpcQueue[json.id];
|
|
139
|
+
} else debug('Unsupported WS message:', json.error.message);
|
|
140
|
+
}
|
|
141
|
+
} catch (e) {
|
|
142
|
+
debug(e);
|
|
143
|
+
debug('Cannot parse msg:', evt.data);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
onOpen(evt) {
|
|
148
|
+
debug('Opened WS'); // subscribe to each addresses (SOL and ASA addr)
|
|
149
|
+
|
|
150
|
+
this.tokensAddresses.concat(this.address).forEach(address => {
|
|
151
|
+
// sub for account state changes
|
|
152
|
+
this.ws.send(JSON.stringify({
|
|
153
|
+
jsonrpc: '2.0',
|
|
154
|
+
method: 'accountSubscribe',
|
|
155
|
+
params: [address, {
|
|
156
|
+
encoding: 'jsonParsed'
|
|
157
|
+
}],
|
|
158
|
+
id: 1
|
|
159
|
+
})); // sub for incoming/outcoming txs
|
|
160
|
+
|
|
161
|
+
this.ws.send(JSON.stringify({
|
|
162
|
+
jsonrpc: '2.0',
|
|
163
|
+
method: 'logsSubscribe',
|
|
164
|
+
params: [{
|
|
165
|
+
mentions: [address]
|
|
166
|
+
}, {
|
|
167
|
+
commitment: 'finalized'
|
|
168
|
+
}],
|
|
169
|
+
id: 2
|
|
170
|
+
}));
|
|
171
|
+
}); // this.doPing()
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
onError(evt) {
|
|
175
|
+
debug('Error on WS:', evt.data);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
onClose(evt) {
|
|
179
|
+
debug('Closing WS');
|
|
180
|
+
clearTimeout(this.pingTimeout);
|
|
181
|
+
clearTimeout(this.reconnectTimeout);
|
|
182
|
+
|
|
183
|
+
if (!this.shutdown) {
|
|
184
|
+
this.doRestart();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async sendMessage(method, params = []) {
|
|
189
|
+
return new Promise((resolve, reject) => {
|
|
190
|
+
if (this.isClosed || this.shutdown) return reject(new Error('connection not started'));
|
|
191
|
+
const id = Math.floor(Math.random() * 1e7) + 1;
|
|
192
|
+
this.rpcQueue[id] = {
|
|
193
|
+
resolve,
|
|
194
|
+
reject
|
|
195
|
+
};
|
|
196
|
+
this.rpcQueue[id].timeout = setTimeout(() => {
|
|
197
|
+
delete this.rpcQueue[id];
|
|
198
|
+
debug(`ws timeout command: ${method} - ${JSON.stringify(params)} - ${id}`);
|
|
199
|
+
reject(new Error('solana ws: reply timeout'));
|
|
200
|
+
}, TIMEOUT);
|
|
201
|
+
if (typeof this.rpcQueue[id].timeout.unref === 'function') this.rpcQueue[id].timeout.unref();
|
|
202
|
+
this.ws.send(JSON.stringify({
|
|
203
|
+
jsonrpc: '2.0',
|
|
204
|
+
method,
|
|
205
|
+
params,
|
|
206
|
+
id
|
|
207
|
+
}));
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async processMessages() {
|
|
212
|
+
if (this.inProcessMessages) return null;
|
|
213
|
+
this.inProcessMessages = true;
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
while (this.messageQueue.length) {
|
|
217
|
+
const items = this.messageQueue.splice(0, this.messageQueue.length);
|
|
218
|
+
await this.callback(items);
|
|
219
|
+
}
|
|
220
|
+
} catch (e) {
|
|
221
|
+
console.log(`Solana: error processing streams: ${e.message}`);
|
|
222
|
+
} finally {
|
|
223
|
+
this.inProcessMessages = false;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async close() {
|
|
228
|
+
clearTimeout(this.reconnectTimeout);
|
|
229
|
+
clearTimeout(this.pingTimeout);
|
|
230
|
+
|
|
231
|
+
if (this.ws && (this.isConnecting || this.isOpen)) {
|
|
232
|
+
// this.ws.send(JSON.stringify({ method: 'close' }))
|
|
233
|
+
await (0, _delay.default)((0, _ms.default)('1s')); // allow for the 'close' round-trip
|
|
234
|
+
|
|
235
|
+
await this.ws.close();
|
|
236
|
+
await this.ws.terminate();
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async start() {
|
|
241
|
+
try {
|
|
242
|
+
if (!this.isClosed || this.shutdown) return;
|
|
243
|
+
this.ws = this.newSocket(this.endpoint);
|
|
244
|
+
} catch (e) {
|
|
245
|
+
console.log('Solana: error starting WS:', e);
|
|
246
|
+
this.doRestart();
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async stop() {
|
|
251
|
+
if (this.shutdown) return;
|
|
252
|
+
this.shutdown = true;
|
|
253
|
+
await this.close();
|
|
254
|
+
|
|
255
|
+
while (this.running) {
|
|
256
|
+
await (0, _delay.default)((0, _ms.default)('1s'));
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
} // Connection
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
exports.Connection = Connection;
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
var _exportNames = {};
|
|
7
|
+
exports.default = void 0;
|
|
8
|
+
|
|
9
|
+
var _basicUtils = require("@exodus/basic-utils");
|
|
10
|
+
|
|
11
|
+
var _assets = require("@exodus/assets");
|
|
12
|
+
|
|
13
|
+
var _solanaMeta = _interopRequireDefault(require("@exodus/solana-meta"));
|
|
14
|
+
|
|
15
|
+
var _api = require("./api");
|
|
16
|
+
|
|
17
|
+
Object.keys(_api).forEach(function (key) {
|
|
18
|
+
if (key === "default" || key === "__esModule") return;
|
|
19
|
+
if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return;
|
|
20
|
+
if (key in exports && exports[key] === _api[key]) return;
|
|
21
|
+
Object.defineProperty(exports, key, {
|
|
22
|
+
enumerable: true,
|
|
23
|
+
get: function () {
|
|
24
|
+
return _api[key];
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
var _txLog = require("./tx-log");
|
|
30
|
+
|
|
31
|
+
Object.keys(_txLog).forEach(function (key) {
|
|
32
|
+
if (key === "default" || key === "__esModule") return;
|
|
33
|
+
if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return;
|
|
34
|
+
if (key in exports && exports[key] === _txLog[key]) return;
|
|
35
|
+
Object.defineProperty(exports, key, {
|
|
36
|
+
enumerable: true,
|
|
37
|
+
get: function () {
|
|
38
|
+
return _txLog[key];
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
var _accountState = require("./account-state");
|
|
44
|
+
|
|
45
|
+
Object.keys(_accountState).forEach(function (key) {
|
|
46
|
+
if (key === "default" || key === "__esModule") return;
|
|
47
|
+
if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return;
|
|
48
|
+
if (key in exports && exports[key] === _accountState[key]) return;
|
|
49
|
+
Object.defineProperty(exports, key, {
|
|
50
|
+
enumerable: true,
|
|
51
|
+
get: function () {
|
|
52
|
+
return _accountState[key];
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
58
|
+
|
|
59
|
+
// These are not the same asset objects as the wallet creates, so they should never be returned to the wallet.
|
|
60
|
+
// Initially this may be violated by the Solana code until the first monitor tick updates assets with setTokens()
|
|
61
|
+
const assets = (0, _assets.connectAssets)((0, _basicUtils.keyBy)(_solanaMeta.default, asset => asset.name)); // At some point we would like to exclude this export. Default export should be the whole asset "plugin" ready to be injected.
|
|
62
|
+
// Clients should not call an specific server api directly.
|
|
63
|
+
|
|
64
|
+
const serverApi = new _api.Api({
|
|
65
|
+
assets
|
|
66
|
+
});
|
|
67
|
+
var _default = serverApi;
|
|
68
|
+
exports.default = _default;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
|
|
7
|
+
var _solanaMonitor = require("./solana-monitor");
|
|
8
|
+
|
|
9
|
+
Object.keys(_solanaMonitor).forEach(function (key) {
|
|
10
|
+
if (key === "default" || key === "__esModule") return;
|
|
11
|
+
if (key in exports && exports[key] === _solanaMonitor[key]) return;
|
|
12
|
+
Object.defineProperty(exports, key, {
|
|
13
|
+
enumerable: true,
|
|
14
|
+
get: function () {
|
|
15
|
+
return _solanaMonitor[key];
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
});
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.SolanaMonitor = void 0;
|
|
7
|
+
|
|
8
|
+
var _assetLib = require("@exodus/asset-lib");
|
|
9
|
+
|
|
10
|
+
var _lodash = _interopRequireDefault(require("lodash"));
|
|
11
|
+
|
|
12
|
+
var _minimalisticAssert = _interopRequireDefault(require("minimalistic-assert"));
|
|
13
|
+
|
|
14
|
+
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
15
|
+
|
|
16
|
+
const DEFAULT_POOL_ADDRESS = '9QU2QSxhb24FUX3Tu2FpczXjpK3VYrvRudywSZaM29mF'; // Everstake
|
|
17
|
+
|
|
18
|
+
const DEFAULT_REMOTE_CONFIG = {
|
|
19
|
+
rpcs: [],
|
|
20
|
+
ws: [],
|
|
21
|
+
staking: {
|
|
22
|
+
enabled: true,
|
|
23
|
+
pool: DEFAULT_POOL_ADDRESS
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
class SolanaMonitor extends _assetLib.BaseMonitor {
|
|
28
|
+
constructor({
|
|
29
|
+
api,
|
|
30
|
+
includeUnparsed = false,
|
|
31
|
+
...args
|
|
32
|
+
}) {
|
|
33
|
+
super(args);
|
|
34
|
+
(0, _minimalisticAssert.default)(api, 'api is required');
|
|
35
|
+
this.api = api;
|
|
36
|
+
this.cursors = {};
|
|
37
|
+
this.assets = {};
|
|
38
|
+
this.includeUnparsed = includeUnparsed;
|
|
39
|
+
this.addHook('before-stop', (...args) => this.beforeStop(...args));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async beforeStop() {
|
|
43
|
+
const walletAccounts = await this.aci.getWalletAccounts({
|
|
44
|
+
assetName: this.asset.name
|
|
45
|
+
});
|
|
46
|
+
return Promise.all(walletAccounts.map(walletAccount => this.stopListener({
|
|
47
|
+
walletAccount
|
|
48
|
+
})));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async initWalletAccount({
|
|
52
|
+
walletAccount
|
|
53
|
+
}) {
|
|
54
|
+
if (this.tickCount[walletAccount] === 0) {
|
|
55
|
+
await this.startListener({
|
|
56
|
+
walletAccount
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async startListener({
|
|
62
|
+
walletAccount
|
|
63
|
+
}) {
|
|
64
|
+
const address = await this.aci.getReceiveAddress({
|
|
65
|
+
assetName: this.asset.name,
|
|
66
|
+
walletAccount
|
|
67
|
+
});
|
|
68
|
+
return this.api.watchAddress({
|
|
69
|
+
address
|
|
70
|
+
/*
|
|
71
|
+
// OPTIONAL. Relying on polling through ws
|
|
72
|
+
tokensAddresses: [], // needed for ASA subs
|
|
73
|
+
handleAccounts: (updates) => this.accountsCallback({ updates, walletAccount }),
|
|
74
|
+
handleTransfers: (txs) => {
|
|
75
|
+
// new SOL tx, ticking monitor
|
|
76
|
+
this.tick({ walletAccount }) // it will cause refresh for both sender/receiver. Without necessarily fetching the tx if it's not finalized in the node.
|
|
77
|
+
},
|
|
78
|
+
*/
|
|
79
|
+
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async stopListener({
|
|
84
|
+
walletAccount
|
|
85
|
+
}) {
|
|
86
|
+
const address = await this.aci.getReceiveAddress({
|
|
87
|
+
assetName: this.asset.name,
|
|
88
|
+
walletAccount
|
|
89
|
+
});
|
|
90
|
+
return this.api.unwatchAddress({
|
|
91
|
+
address
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
setServer(config = {}) {
|
|
96
|
+
const {
|
|
97
|
+
rpcs,
|
|
98
|
+
ws,
|
|
99
|
+
staking = {}
|
|
100
|
+
} = { ...DEFAULT_REMOTE_CONFIG,
|
|
101
|
+
...config
|
|
102
|
+
};
|
|
103
|
+
this.api.setServer(rpcs[0]);
|
|
104
|
+
this.api.setWsEndpoint(ws[0]);
|
|
105
|
+
this.staking = staking;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
hasNewCursor({
|
|
109
|
+
walletAccount,
|
|
110
|
+
cursorState
|
|
111
|
+
}) {
|
|
112
|
+
const {
|
|
113
|
+
cursor
|
|
114
|
+
} = cursorState;
|
|
115
|
+
return this.cursors[walletAccount] !== cursor;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async emitUnknownTokensEvent({
|
|
119
|
+
tokenAccounts
|
|
120
|
+
}) {
|
|
121
|
+
const tokensList = await this.api.getWalletTokensList({
|
|
122
|
+
tokenAccounts
|
|
123
|
+
});
|
|
124
|
+
const unknownTokensList = tokensList.filter(mintAddress => {
|
|
125
|
+
return !this.api.tokens[mintAddress];
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
if (unknownTokensList.length > 0) {
|
|
129
|
+
this.emit('unknown-tokens', unknownTokensList);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async tick({
|
|
134
|
+
walletAccount,
|
|
135
|
+
refresh
|
|
136
|
+
}) {
|
|
137
|
+
// Check for new wallet account
|
|
138
|
+
await this.initWalletAccount({
|
|
139
|
+
walletAccount
|
|
140
|
+
});
|
|
141
|
+
const assetName = this.asset.name;
|
|
142
|
+
this.assets = await this.aci.getAssetsForNetwork({
|
|
143
|
+
baseAssetName: assetName
|
|
144
|
+
});
|
|
145
|
+
this.api.setTokens(this.assets);
|
|
146
|
+
const accountState = await this.aci.getAccountState({
|
|
147
|
+
assetName,
|
|
148
|
+
walletAccount
|
|
149
|
+
});
|
|
150
|
+
const address = await this.aci.getReceiveAddress({
|
|
151
|
+
assetName,
|
|
152
|
+
walletAccount
|
|
153
|
+
});
|
|
154
|
+
const {
|
|
155
|
+
logItemsByAsset,
|
|
156
|
+
hasNewTxs,
|
|
157
|
+
cursorState
|
|
158
|
+
} = await this.getHistory({
|
|
159
|
+
address,
|
|
160
|
+
accountState,
|
|
161
|
+
walletAccount,
|
|
162
|
+
refresh
|
|
163
|
+
});
|
|
164
|
+
let staking = accountState.mem;
|
|
165
|
+
const cursorChanged = this.hasNewCursor({
|
|
166
|
+
walletAccount,
|
|
167
|
+
cursorState
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
if (refresh || cursorChanged) {
|
|
171
|
+
staking = await this.updateStakingInfo({
|
|
172
|
+
walletAccount,
|
|
173
|
+
address
|
|
174
|
+
});
|
|
175
|
+
this.cursors[walletAccount] = cursorState.cursor;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
await this.updateTxLogByAsset({
|
|
179
|
+
walletAccount,
|
|
180
|
+
logItemsByAsset,
|
|
181
|
+
refresh
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
if (refresh || hasNewTxs || cursorChanged) {
|
|
185
|
+
const tokenAccounts = await this.api.getTokenAccountsByOwner(address);
|
|
186
|
+
await this.emitUnknownTokensEvent({
|
|
187
|
+
tokenAccounts
|
|
188
|
+
});
|
|
189
|
+
const account = await this.getAccount({
|
|
190
|
+
address,
|
|
191
|
+
staking,
|
|
192
|
+
tokenAccounts
|
|
193
|
+
});
|
|
194
|
+
await this.updateState({
|
|
195
|
+
account,
|
|
196
|
+
cursorState,
|
|
197
|
+
walletAccount
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async getHistory({
|
|
203
|
+
address,
|
|
204
|
+
accountState,
|
|
205
|
+
refresh
|
|
206
|
+
} = {}) {
|
|
207
|
+
let cursor = refresh ? '' : accountState.cursor;
|
|
208
|
+
const baseAsset = this.asset;
|
|
209
|
+
const {
|
|
210
|
+
transactions,
|
|
211
|
+
newCursor
|
|
212
|
+
} = await this.api.getTransactions(address, {
|
|
213
|
+
cursor,
|
|
214
|
+
includeUnparsed: this.includeUnparsed
|
|
215
|
+
});
|
|
216
|
+
const mappedTransactions = [];
|
|
217
|
+
|
|
218
|
+
for (const tx of transactions) {
|
|
219
|
+
const assetName = _lodash.default.get(tx, 'token.tokenName', baseAsset.name);
|
|
220
|
+
|
|
221
|
+
const asset = this.assets[assetName];
|
|
222
|
+
if (assetName === 'unknown' || !asset) continue; // skip unknown tokens
|
|
223
|
+
|
|
224
|
+
const coinAmount = asset.currency.baseUnit(tx.amount).toDefault();
|
|
225
|
+
const item = {
|
|
226
|
+
coinName: assetName,
|
|
227
|
+
txId: tx.id,
|
|
228
|
+
from: [tx.from],
|
|
229
|
+
coinAmount,
|
|
230
|
+
confirmations: 1,
|
|
231
|
+
// tx.confirmations, // avoid multiple notifications
|
|
232
|
+
date: tx.date,
|
|
233
|
+
error: tx.error,
|
|
234
|
+
data: {
|
|
235
|
+
staking: tx.staking || null,
|
|
236
|
+
unparsed: !!tx.unparsed,
|
|
237
|
+
swapTx: !!(tx.data && tx.data.inner)
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
if (tx.owner === address) {
|
|
242
|
+
// send transaction
|
|
243
|
+
item.to = tx.to;
|
|
244
|
+
item.feeAmount = baseAsset.currency.baseUnit(tx.fee).toDefault(); // in SOL
|
|
245
|
+
|
|
246
|
+
item.coinAmount = item.coinAmount.negate();
|
|
247
|
+
|
|
248
|
+
if (tx.to === tx.owner) {
|
|
249
|
+
item.selfSend = true;
|
|
250
|
+
item.coinAmount = asset.currency.ZERO;
|
|
251
|
+
}
|
|
252
|
+
} else if (tx.unparsed) {
|
|
253
|
+
if (tx.fee !== 0) item.feeAmount = baseAsset.currency.baseUnit(tx.fee).toDefault(); // in SOL
|
|
254
|
+
|
|
255
|
+
item.data.meta = tx.data.meta;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (asset.assetType === 'SOLANA_TOKEN' && item.feeAmount && item.feeAmount.isPositive) {
|
|
259
|
+
const feeAsset = asset.feeAsset;
|
|
260
|
+
const feeItem = { ..._lodash.default.clone(item),
|
|
261
|
+
coinName: feeAsset.name,
|
|
262
|
+
tokens: [asset.name],
|
|
263
|
+
coinAmount: feeAsset.currency.ZERO
|
|
264
|
+
};
|
|
265
|
+
mappedTransactions.push(feeItem);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
mappedTransactions.push(item);
|
|
269
|
+
} // logItemsByAsset = { 'solana:': [...], 'serum': [...] }
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
logItemsByAsset: _lodash.default.groupBy(mappedTransactions, item => item.coinName),
|
|
274
|
+
hasNewTxs: transactions.length > 0,
|
|
275
|
+
cursorState: {
|
|
276
|
+
cursor: newCursor
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async getAccount({
|
|
282
|
+
address,
|
|
283
|
+
staking,
|
|
284
|
+
tokenAccounts
|
|
285
|
+
}) {
|
|
286
|
+
const tokens = Object.keys(this.assets).filter(name => name !== this.asset.name);
|
|
287
|
+
const [solBalance, splBalances] = await Promise.all([this.api.getBalance(address), this.api.getTokensBalance({
|
|
288
|
+
address,
|
|
289
|
+
filterByTokens: tokens,
|
|
290
|
+
tokenAccounts
|
|
291
|
+
})]);
|
|
292
|
+
const stakedBalance = this.asset.currency.baseUnit(staking.locked).toDefault();
|
|
293
|
+
const withdrawableBalance = this.asset.currency.baseUnit(staking.withdrawable).toDefault();
|
|
294
|
+
const pendingBalance = this.asset.currency.baseUnit(staking.pending).toDefault();
|
|
295
|
+
const balance = this.asset.currency.baseUnit(solBalance).toDefault().add(stakedBalance).add(withdrawableBalance).add(pendingBalance);
|
|
296
|
+
|
|
297
|
+
const tokenBalances = _lodash.default.mapValues(splBalances, (balance, name) => this.assets[name].currency.baseUnit(balance).toDefault());
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
balance,
|
|
301
|
+
tokenBalances
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async updateState({
|
|
306
|
+
account,
|
|
307
|
+
cursorState,
|
|
308
|
+
walletAccount
|
|
309
|
+
}) {
|
|
310
|
+
const {
|
|
311
|
+
balance,
|
|
312
|
+
tokenBalances
|
|
313
|
+
} = account;
|
|
314
|
+
const newData = {
|
|
315
|
+
balance,
|
|
316
|
+
tokenBalances,
|
|
317
|
+
...cursorState
|
|
318
|
+
};
|
|
319
|
+
return this.updateAccountState({
|
|
320
|
+
newData,
|
|
321
|
+
walletAccount
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async updateStakingInfo({
|
|
326
|
+
walletAccount,
|
|
327
|
+
address
|
|
328
|
+
}) {
|
|
329
|
+
const stakingInfo = await this.api.getStakeAccountsInfo(address);
|
|
330
|
+
const rewards = await this.api.getRewards(Object.keys(stakingInfo.accounts));
|
|
331
|
+
const mem = {
|
|
332
|
+
loaded: true,
|
|
333
|
+
staking: this.staking,
|
|
334
|
+
isDelegating: Object.values(stakingInfo.accounts).some(({
|
|
335
|
+
state
|
|
336
|
+
}) => ['active', 'activating', 'inactive'].includes(state)),
|
|
337
|
+
// true if at least 1 account is delegating
|
|
338
|
+
locked: this.asset.currency.baseUnit(stakingInfo.locked).toDefault(),
|
|
339
|
+
withdrawable: this.asset.currency.baseUnit(stakingInfo.withdrawable).toDefault(),
|
|
340
|
+
pending: this.asset.currency.baseUnit(stakingInfo.pending).toDefault(),
|
|
341
|
+
// still undelegating (not yet available for withdraw)
|
|
342
|
+
earned: this.asset.currency.baseUnit(rewards).toDefault(),
|
|
343
|
+
accounts: stakingInfo.accounts // Obj
|
|
344
|
+
|
|
345
|
+
};
|
|
346
|
+
await this.updateAccountState({
|
|
347
|
+
walletAccount,
|
|
348
|
+
newData: {
|
|
349
|
+
mem
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
return mem;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
exports.SolanaMonitor = SolanaMonitor;
|