@exodus/solana-api 2.5.18 → 2.5.20
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 +48 -0
- package/lib/api.js +1161 -0
- package/lib/connection.js +262 -0
- package/lib/index.js +68 -0
- package/lib/pay/fetchTransaction.js +157 -0
- package/lib/pay/index.js +57 -0
- package/lib/pay/parseURL.js +120 -0
- package/lib/pay/prepareSendData.js +38 -0
- package/lib/pay/validateBeforePay.js +44 -0
- package/lib/tx-log/index.js +18 -0
- package/lib/tx-log/solana-monitor.js +354 -0
- package/package.json +3 -3
- package/src/__tests__/api.test.js +286 -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 +74 -0
- package/src/api.js +1 -1
- package/src/connection.js +21 -16
- package/src/tx-log/__tests__/solana-monitor-api-mock.js +353 -0
- package/src/tx-log/__tests__/solana-monitor.integration.test.js +119 -0
- package/src/tx-log/__tests__/solana-monitor.test.js +132 -0
|
@@ -0,0 +1,262 @@
|
|
|
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).unref();
|
|
201
|
+
this.ws.send(JSON.stringify({
|
|
202
|
+
jsonrpc: '2.0',
|
|
203
|
+
method,
|
|
204
|
+
params,
|
|
205
|
+
id
|
|
206
|
+
}));
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async processMessages() {
|
|
211
|
+
if (this.inProcessMessages) return null;
|
|
212
|
+
this.inProcessMessages = true;
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
while (this.messageQueue.length) {
|
|
216
|
+
const items = this.messageQueue.splice(0, this.messageQueue.length);
|
|
217
|
+
await this.callback(items);
|
|
218
|
+
}
|
|
219
|
+
} catch (e) {
|
|
220
|
+
console.log(`Solana: error processing streams: ${e.message}`);
|
|
221
|
+
} finally {
|
|
222
|
+
this.inProcessMessages = false;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async close() {
|
|
227
|
+
clearTimeout(this.reconnectTimeout);
|
|
228
|
+
clearTimeout(this.pingTimeout);
|
|
229
|
+
|
|
230
|
+
if (this.ws && (this.isConnecting || this.isOpen)) {
|
|
231
|
+
// this.ws.send(JSON.stringify({ method: 'close' }))
|
|
232
|
+
await (0, _delay.default)((0, _ms.default)('1s')); // allow for the 'close' round-trip
|
|
233
|
+
|
|
234
|
+
await this.ws.close();
|
|
235
|
+
await this.ws.terminate();
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async start() {
|
|
240
|
+
try {
|
|
241
|
+
if (!this.isClosed || this.shutdown) return;
|
|
242
|
+
this.ws = this.newSocket(this.endpoint);
|
|
243
|
+
} catch (e) {
|
|
244
|
+
console.log('Solana: error starting WS:', e);
|
|
245
|
+
this.doRestart();
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async stop() {
|
|
250
|
+
if (this.shutdown) return;
|
|
251
|
+
this.shutdown = true;
|
|
252
|
+
await this.close();
|
|
253
|
+
|
|
254
|
+
while (this.running) {
|
|
255
|
+
await (0, _delay.default)((0, _ms.default)('1s'));
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
} // Connection
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
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 _api = require("./api");
|
|
10
|
+
|
|
11
|
+
Object.keys(_api).forEach(function (key) {
|
|
12
|
+
if (key === "default" || key === "__esModule") return;
|
|
13
|
+
if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return;
|
|
14
|
+
if (key in exports && exports[key] === _api[key]) return;
|
|
15
|
+
Object.defineProperty(exports, key, {
|
|
16
|
+
enumerable: true,
|
|
17
|
+
get: function () {
|
|
18
|
+
return _api[key];
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
var _txLog = require("./tx-log");
|
|
24
|
+
|
|
25
|
+
Object.keys(_txLog).forEach(function (key) {
|
|
26
|
+
if (key === "default" || key === "__esModule") return;
|
|
27
|
+
if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return;
|
|
28
|
+
if (key in exports && exports[key] === _txLog[key]) return;
|
|
29
|
+
Object.defineProperty(exports, key, {
|
|
30
|
+
enumerable: true,
|
|
31
|
+
get: function () {
|
|
32
|
+
return _txLog[key];
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
var _accountState = require("./account-state");
|
|
38
|
+
|
|
39
|
+
Object.keys(_accountState).forEach(function (key) {
|
|
40
|
+
if (key === "default" || key === "__esModule") return;
|
|
41
|
+
if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return;
|
|
42
|
+
if (key in exports && exports[key] === _accountState[key]) return;
|
|
43
|
+
Object.defineProperty(exports, key, {
|
|
44
|
+
enumerable: true,
|
|
45
|
+
get: function () {
|
|
46
|
+
return _accountState[key];
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
var _pay = require("./pay");
|
|
52
|
+
|
|
53
|
+
Object.keys(_pay).forEach(function (key) {
|
|
54
|
+
if (key === "default" || key === "__esModule") return;
|
|
55
|
+
if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return;
|
|
56
|
+
if (key in exports && exports[key] === _pay[key]) return;
|
|
57
|
+
Object.defineProperty(exports, key, {
|
|
58
|
+
enumerable: true,
|
|
59
|
+
get: function () {
|
|
60
|
+
return _pay[key];
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
// At some point we would like to exclude this export. Default export should be the whole asset "plugin" ready to be injected.
|
|
65
|
+
// Clients should not call an specific server api directly.
|
|
66
|
+
const serverApi = new _api.Api();
|
|
67
|
+
var _default = serverApi;
|
|
68
|
+
exports.default = _default;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.ParseTransactionError = exports.FetchTransactionError = void 0;
|
|
7
|
+
exports.fetchTransaction = fetchTransaction;
|
|
8
|
+
|
|
9
|
+
var _tweetnacl = _interopRequireDefault(require("tweetnacl"));
|
|
10
|
+
|
|
11
|
+
var _fetch = require("@exodus/fetch");
|
|
12
|
+
|
|
13
|
+
var _ms = _interopRequireDefault(require("ms"));
|
|
14
|
+
|
|
15
|
+
var _solanaWeb = require("@exodus/solana-web3.js");
|
|
16
|
+
|
|
17
|
+
var _ = _interopRequireDefault(require(".."));
|
|
18
|
+
|
|
19
|
+
var _bignumber = _interopRequireDefault(require("bignumber.js"));
|
|
20
|
+
|
|
21
|
+
var _solanaLib = require("@exodus/solana-lib");
|
|
22
|
+
|
|
23
|
+
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
24
|
+
|
|
25
|
+
class FetchTransactionError extends Error {
|
|
26
|
+
constructor(...args) {
|
|
27
|
+
super(...args);
|
|
28
|
+
this.name = 'FetchTransactionError';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
exports.FetchTransactionError = FetchTransactionError;
|
|
34
|
+
|
|
35
|
+
class ParseTransactionError extends Error {
|
|
36
|
+
constructor(...args) {
|
|
37
|
+
super(...args);
|
|
38
|
+
this.name = 'ParseTransactionError';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
exports.ParseTransactionError = ParseTransactionError;
|
|
44
|
+
|
|
45
|
+
const isTransferCheckedInstruction = decodedInstruction => decodedInstruction.type === 'transferChecked';
|
|
46
|
+
|
|
47
|
+
const isTransferInstruction = decodedInstruction => decodedInstruction.type === 'transfer';
|
|
48
|
+
|
|
49
|
+
const isSplAccount = account => account && account.owner === 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA';
|
|
50
|
+
|
|
51
|
+
async function fetchTransaction({
|
|
52
|
+
account,
|
|
53
|
+
link,
|
|
54
|
+
commitment
|
|
55
|
+
}) {
|
|
56
|
+
const response = await (0, _fetch.fetchival)(String(link), {
|
|
57
|
+
mode: 'cors',
|
|
58
|
+
cache: 'no-cache',
|
|
59
|
+
credentials: 'omit',
|
|
60
|
+
timeout: (0, _ms.default)('10s'),
|
|
61
|
+
headers: {
|
|
62
|
+
Accept: 'application/json',
|
|
63
|
+
'Content-Type': 'application/json'
|
|
64
|
+
}
|
|
65
|
+
}).post({
|
|
66
|
+
account
|
|
67
|
+
});
|
|
68
|
+
if (!response || !response.transaction) throw new FetchTransactionError('missing transaction');
|
|
69
|
+
const {
|
|
70
|
+
transaction: txString
|
|
71
|
+
} = response;
|
|
72
|
+
if (typeof txString !== 'string') throw new FetchTransactionError('invalid transaction');
|
|
73
|
+
|
|
74
|
+
const transaction = _solanaWeb.Transaction.from(Buffer.from(txString, 'base64'));
|
|
75
|
+
|
|
76
|
+
const {
|
|
77
|
+
signatures,
|
|
78
|
+
feePayer,
|
|
79
|
+
recentBlockhash
|
|
80
|
+
} = transaction;
|
|
81
|
+
|
|
82
|
+
if (signatures.length) {
|
|
83
|
+
if (!feePayer) throw new FetchTransactionError('missing fee payer');
|
|
84
|
+
if (!feePayer.equals(signatures[0].publicKey)) throw new FetchTransactionError('invalid fee payer');
|
|
85
|
+
if (!recentBlockhash) throw new FetchTransactionError('missing recent blockhash'); // A valid signature for everything except `account` must be provided.
|
|
86
|
+
|
|
87
|
+
const message = transaction.serializeMessage();
|
|
88
|
+
|
|
89
|
+
for (const {
|
|
90
|
+
signature,
|
|
91
|
+
publicKey
|
|
92
|
+
} of signatures) {
|
|
93
|
+
if (signature) {
|
|
94
|
+
if (!_tweetnacl.default.sign.detached.verify(message, signature, publicKey.toBuffer())) throw new FetchTransactionError('invalid signature');
|
|
95
|
+
} else if (publicKey.equals(new _solanaWeb.PublicKey(account))) {
|
|
96
|
+
// If the only signature expected is for `account`, ignore the recent blockhash in the transaction.
|
|
97
|
+
if (signatures.length === 1) {
|
|
98
|
+
transaction.recentBlockhash = await _.default.getRecentBlockHash(commitment);
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
throw new FetchTransactionError('missing signature');
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
// Ignore the fee payer and recent blockhash in the transaction and initialize them.
|
|
106
|
+
transaction.feePayer = account;
|
|
107
|
+
transaction.recentBlockhash = await _.default.getRecentBlockHash(commitment);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return parseInstructions(transaction);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function parseInstructions(transaction) {
|
|
114
|
+
// Make a copy of the instructions we're going to mutate it.
|
|
115
|
+
const instructions = transaction.instructions.slice();
|
|
116
|
+
if (!instructions || !Array.isArray(instructions) || instructions.length !== 1) throw new ParseTransactionError('Invalid transaction instructions'); // Transfer instruction must be the last instruction
|
|
117
|
+
|
|
118
|
+
const instruction = instructions.pop();
|
|
119
|
+
if (!instruction) throw new ParseTransactionError('missing transfer instruction');
|
|
120
|
+
const isTokenTransfer = instruction.programId.equals(_solanaLib.TOKEN_PROGRAM_ID);
|
|
121
|
+
const isSolNativeTransfer = instruction.programId.equals(_solanaLib.SYSTEM_PROGRAM_ID);
|
|
122
|
+
|
|
123
|
+
if (isTokenTransfer) {
|
|
124
|
+
const decodedInstruction = (0, _solanaLib.decodeTokenProgramInstruction)(instruction);
|
|
125
|
+
if (!isTransferCheckedInstruction(decodedInstruction) && !isTransferInstruction(decodedInstruction)) throw new ParseTransactionError('invalid token transfer');
|
|
126
|
+
const [, mint, destination, owner] = instruction.keys;
|
|
127
|
+
const splToken = mint.pubkey.toBase58();
|
|
128
|
+
let asset;
|
|
129
|
+
let recipient = destination.pubkey.toBase58();
|
|
130
|
+
|
|
131
|
+
if (splToken) {
|
|
132
|
+
if (!_.default.isTokenSupported(splToken)) throw new ParseTransactionError(`spl-token ${splToken} is not supported`);
|
|
133
|
+
asset = _.default.getTokenByAddress(splToken);
|
|
134
|
+
const account = await _.default.getAccountInfo(recipient);
|
|
135
|
+
if (isSplAccount(account)) recipient = account.data.parsed.info.owner;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
amount: asset.currency.baseUnit(new _bignumber.default(decodedInstruction.data.amount).toString()).toDefaultString(),
|
|
140
|
+
decimals: decodedInstruction.data.decimals,
|
|
141
|
+
recipient,
|
|
142
|
+
sender: owner.pubkey.toBase58(),
|
|
143
|
+
splToken,
|
|
144
|
+
asset
|
|
145
|
+
};
|
|
146
|
+
} else if (isSolNativeTransfer) {
|
|
147
|
+
const decodedTransaction = _solanaWeb.SystemInstruction.decodeTransfer(instruction);
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
sender: decodedTransaction.fromPubkey.toString(),
|
|
151
|
+
amount: decodedTransaction.lamports.toString(),
|
|
152
|
+
recipient: decodedTransaction.toPubkey.toString()
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
throw new Error('Invalid transfer instruction');
|
|
157
|
+
}
|
package/lib/pay/index.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
|
|
7
|
+
var _parseURL = require("./parseURL.js");
|
|
8
|
+
|
|
9
|
+
Object.keys(_parseURL).forEach(function (key) {
|
|
10
|
+
if (key === "default" || key === "__esModule") return;
|
|
11
|
+
if (key in exports && exports[key] === _parseURL[key]) return;
|
|
12
|
+
Object.defineProperty(exports, key, {
|
|
13
|
+
enumerable: true,
|
|
14
|
+
get: function () {
|
|
15
|
+
return _parseURL[key];
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
var _prepareSendData = require("./prepareSendData.js");
|
|
21
|
+
|
|
22
|
+
Object.keys(_prepareSendData).forEach(function (key) {
|
|
23
|
+
if (key === "default" || key === "__esModule") return;
|
|
24
|
+
if (key in exports && exports[key] === _prepareSendData[key]) return;
|
|
25
|
+
Object.defineProperty(exports, key, {
|
|
26
|
+
enumerable: true,
|
|
27
|
+
get: function () {
|
|
28
|
+
return _prepareSendData[key];
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
var _validateBeforePay = require("./validateBeforePay");
|
|
34
|
+
|
|
35
|
+
Object.keys(_validateBeforePay).forEach(function (key) {
|
|
36
|
+
if (key === "default" || key === "__esModule") return;
|
|
37
|
+
if (key in exports && exports[key] === _validateBeforePay[key]) return;
|
|
38
|
+
Object.defineProperty(exports, key, {
|
|
39
|
+
enumerable: true,
|
|
40
|
+
get: function () {
|
|
41
|
+
return _validateBeforePay[key];
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
var _fetchTransaction = require("./fetchTransaction");
|
|
47
|
+
|
|
48
|
+
Object.keys(_fetchTransaction).forEach(function (key) {
|
|
49
|
+
if (key === "default" || key === "__esModule") return;
|
|
50
|
+
if (key in exports && exports[key] === _fetchTransaction[key]) return;
|
|
51
|
+
Object.defineProperty(exports, key, {
|
|
52
|
+
enumerable: true,
|
|
53
|
+
get: function () {
|
|
54
|
+
return _fetchTransaction[key];
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.ParseURLError = void 0;
|
|
7
|
+
exports.parseURL = parseURL;
|
|
8
|
+
|
|
9
|
+
var _solanaLib = require("@exodus/solana-lib");
|
|
10
|
+
|
|
11
|
+
var _bignumber = _interopRequireDefault(require("bignumber.js"));
|
|
12
|
+
|
|
13
|
+
var _assets = _interopRequireDefault(require("@exodus/assets"));
|
|
14
|
+
|
|
15
|
+
var _urlSearchParams = _interopRequireDefault(require("@ungap/url-search-params"));
|
|
16
|
+
|
|
17
|
+
var _ = _interopRequireDefault(require(".."));
|
|
18
|
+
|
|
19
|
+
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
20
|
+
|
|
21
|
+
const SOLANA_PROTOCOL = 'solana:';
|
|
22
|
+
const HTTPS_PROTOCOL = 'https:';
|
|
23
|
+
|
|
24
|
+
class ParseURLError extends Error {
|
|
25
|
+
constructor(...args) {
|
|
26
|
+
super(...args);
|
|
27
|
+
this.name = 'ParseURLError';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
exports.ParseURLError = ParseURLError;
|
|
33
|
+
|
|
34
|
+
function parseURL(url) {
|
|
35
|
+
if (typeof url === 'string') {
|
|
36
|
+
if (url.length > 2048) throw new ParseURLError('length invalid');
|
|
37
|
+
url = new URL(url);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (url.protocol !== SOLANA_PROTOCOL) throw new ParseURLError('protocol invalid');
|
|
41
|
+
if (!url.pathname) throw new ParseURLError('pathname missing');
|
|
42
|
+
return /[:%]/.test(url.pathname) ? parseTransactionRequestURL(url) : parseTransferRequestURL(url);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseTransactionRequestURL(url) {
|
|
46
|
+
const link = new URL(decodeURIComponent(url.pathname));
|
|
47
|
+
const searchParams = new _urlSearchParams.default(url.search);
|
|
48
|
+
if (link.protocol !== HTTPS_PROTOCOL) throw new ParseURLError('link invalid');
|
|
49
|
+
const label = searchParams.get('label') || undefined;
|
|
50
|
+
const message = searchParams.get('message') || undefined;
|
|
51
|
+
return {
|
|
52
|
+
link,
|
|
53
|
+
label,
|
|
54
|
+
message
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function parseTransferRequestURL(url) {
|
|
59
|
+
let recipient;
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
recipient = new _solanaLib.PublicKey(url.pathname);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
throw new Error('ParseURLError: recipient invalid');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const searchParams = new _urlSearchParams.default(url.search);
|
|
68
|
+
let amount;
|
|
69
|
+
const amountParam = searchParams.get('amount');
|
|
70
|
+
|
|
71
|
+
if (amountParam) {
|
|
72
|
+
if (!/^\d+(\.\d+)?$/.test(amountParam)) throw new Error('ParseURLError: amount invalid');
|
|
73
|
+
amount = new _bignumber.default(amountParam);
|
|
74
|
+
if (amount.isNaN()) throw new Error('ParseURLError: amount NaN');
|
|
75
|
+
if (amount.isNegative()) throw new Error('ParseURLError: amount negative');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let splToken;
|
|
79
|
+
const splTokenParam = searchParams.get('spl-token');
|
|
80
|
+
|
|
81
|
+
if (splTokenParam) {
|
|
82
|
+
try {
|
|
83
|
+
splToken = new _solanaLib.PublicKey(splTokenParam);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
throw new ParseURLError('spl-token invalid');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let asset = _assets.default.solana;
|
|
90
|
+
|
|
91
|
+
if (splToken) {
|
|
92
|
+
if (!_.default.isTokenSupported(splToken)) throw new ParseURLError(`spl-token ${splToken} is not supported`);
|
|
93
|
+
asset = _.default.getTokenByAddress(splToken);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let reference;
|
|
97
|
+
const referenceParams = searchParams.getAll('reference');
|
|
98
|
+
|
|
99
|
+
if (referenceParams.length) {
|
|
100
|
+
try {
|
|
101
|
+
reference = referenceParams.map(reference => new _solanaLib.PublicKey(reference));
|
|
102
|
+
} catch (error) {
|
|
103
|
+
throw new ParseURLError('reference invalid');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const label = searchParams.get('label') || undefined;
|
|
108
|
+
const message = searchParams.get('message') || undefined;
|
|
109
|
+
const memo = searchParams.get('memo') || undefined;
|
|
110
|
+
return {
|
|
111
|
+
asset,
|
|
112
|
+
recipient: recipient.toString(),
|
|
113
|
+
amount: amount ? amount.toString(10) : undefined,
|
|
114
|
+
splToken: splToken ? splToken.toString() : undefined,
|
|
115
|
+
reference,
|
|
116
|
+
label,
|
|
117
|
+
message,
|
|
118
|
+
memo
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.prepareSendData = prepareSendData;
|
|
7
|
+
|
|
8
|
+
var _assert = _interopRequireDefault(require("assert"));
|
|
9
|
+
|
|
10
|
+
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
11
|
+
|
|
12
|
+
function prepareSendData(parsedData, {
|
|
13
|
+
asset,
|
|
14
|
+
feeAmount,
|
|
15
|
+
walletAccount
|
|
16
|
+
}) {
|
|
17
|
+
const {
|
|
18
|
+
amount: amountStr,
|
|
19
|
+
recipient,
|
|
20
|
+
splToken,
|
|
21
|
+
...options
|
|
22
|
+
} = parsedData;
|
|
23
|
+
(0, _assert.default)(amountStr, 'PrepareTxError: Missing amount');
|
|
24
|
+
(0, _assert.default)(recipient, 'PrepareTxError: Missing recipient');
|
|
25
|
+
const amount = asset.currency.defaultUnit(amountStr);
|
|
26
|
+
return {
|
|
27
|
+
asset: asset.name,
|
|
28
|
+
baseAsset: asset.baseAsset,
|
|
29
|
+
customMintAddress: splToken,
|
|
30
|
+
feeAmount,
|
|
31
|
+
receiver: {
|
|
32
|
+
address: recipient,
|
|
33
|
+
amount
|
|
34
|
+
},
|
|
35
|
+
walletAccount,
|
|
36
|
+
...options
|
|
37
|
+
};
|
|
38
|
+
}
|