minimal-xec-wallet 1.0.1

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.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +241 -0
  3. package/dist/minimal-xec-wallet.js +66268 -0
  4. package/dist/minimal-xec-wallet.min.js +55 -0
  5. package/examples/README.md +380 -0
  6. package/examples/advanced/browser-compatibility-test.js +263 -0
  7. package/examples/advanced/get-xec-price.js +149 -0
  8. package/examples/advanced/optimize-utxos.js +255 -0
  9. package/examples/advanced/send-op-return.js +216 -0
  10. package/examples/browser-test.html +350 -0
  11. package/examples/key-management/derive-addresses.js +191 -0
  12. package/examples/key-management/export-to-wif.js +114 -0
  13. package/examples/key-management/validate-address.js +214 -0
  14. package/examples/optimization/simple-consolidation-test.js +79 -0
  15. package/examples/optimization/test-utxo-consolidation.js +179 -0
  16. package/examples/test-examples.js +1204 -0
  17. package/examples/tokens/burn-tokens.js +293 -0
  18. package/examples/tokens/get-token-balance.js +169 -0
  19. package/examples/tokens/get-token-info.js +269 -0
  20. package/examples/tokens/list-all-tokens.js +162 -0
  21. package/examples/tokens/send-any-token.js +260 -0
  22. package/examples/tokens/test-main-wallet-integration.js +193 -0
  23. package/examples/transactions/send-all-xec.js +205 -0
  24. package/examples/transactions/send-to-multiple.js +217 -0
  25. package/examples/transactions/send-xec.js +191 -0
  26. package/examples/utils/show-qr.js +119 -0
  27. package/examples/utils/wallet-helper.js +176 -0
  28. package/examples/validation/comprehensive-infrastructure-test.js +210 -0
  29. package/examples/wallet-creation/create-new-wallet.js +67 -0
  30. package/examples/wallet-creation/import-from-wif.js +135 -0
  31. package/examples/wallet-creation/restore-from-mnemonic.js +100 -0
  32. package/examples/wallet-info/get-balance.js +99 -0
  33. package/examples/wallet-info/get-transactions.js +157 -0
  34. package/examples/wallet-info/get-utxos.js +145 -0
  35. package/examples/wallet.json +11 -0
  36. package/lib/adapters/robust-chronik-router.js +507 -0
  37. package/lib/adapters/router.js +651 -0
  38. package/lib/alp-token-handler.js +581 -0
  39. package/lib/browser-wasm-loader.js +271 -0
  40. package/lib/consolidate-utxos.js +338 -0
  41. package/lib/hybrid-token-manager.js +322 -0
  42. package/lib/key-derivation.js +466 -0
  43. package/lib/op-return.js +314 -0
  44. package/lib/security.js +270 -0
  45. package/lib/send-xec.js +396 -0
  46. package/lib/slp-token-handler.js +572 -0
  47. package/lib/token-protocol-detector.js +307 -0
  48. package/lib/utxos.js +303 -0
  49. package/package.json +125 -0
@@ -0,0 +1,314 @@
1
+ /*
2
+ This library handles OP_RETURN operations for XEC transactions.
3
+ Uses same patterns as send-xec.js for consistency.
4
+ */
5
+
6
+ const { TxBuilder, P2PKHSignatory, fromHex, toHex, Ecc, Script, ALL_BIP143 } = require('ecash-lib')
7
+ const { decodeCashAddress } = require('ecashaddrjs')
8
+ const KeyDerivation = require('./key-derivation')
9
+ const SecurityValidator = require('./security')
10
+
11
+ class OpReturn {
12
+ constructor (localConfig = {}) {
13
+ this.chronik = localConfig.chronik
14
+ this.ar = localConfig.ar
15
+
16
+ if (!this.chronik) {
17
+ throw new Error('Chronik client required for OP_RETURN transactions')
18
+ }
19
+
20
+ if (!this.ar) {
21
+ throw new Error('AdapterRouter required for OP_RETURN transactions')
22
+ }
23
+
24
+ // Initialize components (same as send-xec.js)
25
+ this.keyDerivation = new KeyDerivation()
26
+ this.security = new SecurityValidator(localConfig.security)
27
+
28
+ // Initialize ECC for ecash-lib
29
+ try {
30
+ this.ecc = new Ecc()
31
+ } catch (err) {
32
+ throw new Error(`Ecc initialization failed: ${err.message}`)
33
+ }
34
+
35
+ // Configuration - XEC uses 546 satoshis (5.46 XEC) as standard dust limit
36
+ this.dustLimit = localConfig.dustLimit || 546
37
+ this.maxOpReturnSize = localConfig.maxOpReturnSize || 223 // Max OP_RETURN size in bytes
38
+ this.defaultSatsPerByte = localConfig.defaultSatsPerByte || 1.2
39
+ }
40
+
41
+ async sendOpReturn (walletInfo, xecUtxos, msg, prefix = '6d02', xecOutput = [], satsPerByte = this.defaultSatsPerByte) {
42
+ try {
43
+ // Create OP_RETURN transaction
44
+ const txHex = await this.createOpReturnTx(walletInfo, xecUtxos, msg, prefix, xecOutput, satsPerByte)
45
+
46
+ // Broadcast transaction
47
+ const txid = await this.ar.sendTx(txHex)
48
+
49
+ return txid
50
+ } catch (err) {
51
+ throw new Error(`OP_RETURN send failed: ${err.message}`)
52
+ }
53
+ }
54
+
55
+ async createOpReturnTx (walletInfo, xecUtxos, msg, prefix = '6d02', xecOutput = [], satsPerByte = this.defaultSatsPerByte) {
56
+ try {
57
+ // Validate inputs
58
+ if (!walletInfo || !walletInfo.xecAddress) {
59
+ throw new Error('Valid wallet info required')
60
+ }
61
+
62
+ if (!xecUtxos || xecUtxos.length === 0) {
63
+ throw new Error('UTXOs required for OP_RETURN transaction')
64
+ }
65
+
66
+ // Build OP_RETURN script
67
+ const opReturnScript = this.buildOpReturnScript(msg, prefix)
68
+
69
+ // Calculate total output amount (excluding OP_RETURN which is always 0)
70
+ let totalOutputAmount = 0
71
+ for (const output of xecOutput) {
72
+ if (!output.address || typeof output.amountSat !== 'number') {
73
+ throw new Error('Invalid XEC output format')
74
+ }
75
+ totalOutputAmount += output.amountSat
76
+ }
77
+
78
+ // Select UTXOs using the same logic as send-xec.js
79
+ const coinSelection = this._selectUtxosForOpReturn(
80
+ totalOutputAmount,
81
+ xecUtxos,
82
+ satsPerByte,
83
+ xecOutput.length + 1 // +1 for OP_RETURN output
84
+ )
85
+
86
+ // Get private key (prefer mnemonic - same as send-xec.js)
87
+ let privateKeyHex
88
+ if (walletInfo.mnemonic) {
89
+ const keyData = this.keyDerivation.deriveFromMnemonic(walletInfo.mnemonic, walletInfo.hdPath)
90
+ privateKeyHex = keyData.privateKey
91
+ } else {
92
+ privateKeyHex = walletInfo.privateKey
93
+ }
94
+
95
+ const sk = fromHex(privateKeyHex)
96
+ const pk = this.ecc.derivePubkey(sk)
97
+
98
+ // Build outputs array (same pattern as send-xec.js)
99
+ const txOutputs = []
100
+
101
+ // Add OP_RETURN output first (0 value)
102
+ txOutputs.push({
103
+ sats: BigInt(0),
104
+ script: new Script(opReturnScript)
105
+ })
106
+
107
+ // Add XEC outputs
108
+ for (const output of xecOutput) {
109
+ const decoded = decodeCashAddress(output.address)
110
+ txOutputs.push({
111
+ sats: BigInt(output.amountSat),
112
+ script: Script.p2pkh(fromHex(decoded.hash))
113
+ })
114
+ }
115
+
116
+ // Add change address for automatic calculation (same as send-xec.js)
117
+ const walletDecoded = decodeCashAddress(walletInfo.xecAddress)
118
+ txOutputs.push(Script.p2pkh(fromHex(walletDecoded.hash)))
119
+
120
+ // Build inputs (same pattern as send-xec.js)
121
+ const inputs = coinSelection.necessaryUtxos.map(utxo => ({
122
+ input: {
123
+ prevOut: {
124
+ txid: utxo.outpoint.txid,
125
+ outIdx: utxo.outpoint.outIdx
126
+ },
127
+ signData: {
128
+ sats: BigInt(this._getUtxoValue(utxo)),
129
+ outputScript: Script.p2pkh(fromHex(walletDecoded.hash))
130
+ }
131
+ },
132
+ signatory: P2PKHSignatory(sk, pk, ALL_BIP143)
133
+ }))
134
+
135
+ // Build and sign transaction (same as send-xec.js)
136
+ const txBuilder = new TxBuilder({ inputs, outputs: txOutputs })
137
+ const tx = txBuilder.sign({
138
+ feePerKb: BigInt(Math.round(satsPerByte * 1000)),
139
+ dustSats: BigInt(this.dustLimit)
140
+ })
141
+
142
+ return toHex(tx.ser())
143
+ } catch (err) {
144
+ throw new Error(`OP_RETURN transaction creation failed: ${err.message}`)
145
+ }
146
+ }
147
+
148
+ buildOpReturnScript (msg, prefix = '6d02') {
149
+ try {
150
+ // Convert message to buffer
151
+ const msgBuffer = Buffer.isBuffer(msg) ? msg : Buffer.from(msg, 'utf8')
152
+
153
+ // Convert prefix from hex if it's a string
154
+ const prefixBuffer = typeof prefix === 'string' ? Buffer.from(prefix, 'hex') : prefix
155
+
156
+ // Calculate total size
157
+ const totalSize = prefixBuffer.length + msgBuffer.length
158
+
159
+ if (totalSize > this.maxOpReturnSize) {
160
+ throw new Error(`OP_RETURN data too large: ${totalSize} bytes (max: ${this.maxOpReturnSize})`)
161
+ }
162
+
163
+ // Build OP_RETURN script
164
+ // Format: OP_RETURN <prefix> <message>
165
+ const script = Buffer.alloc(2 + prefixBuffer.length + msgBuffer.length)
166
+ let offset = 0
167
+
168
+ // OP_RETURN opcode
169
+ script[offset++] = 0x6a
170
+
171
+ // Push data opcode based on total size
172
+ if (totalSize <= 75) {
173
+ script[offset++] = totalSize
174
+ } else if (totalSize <= 255) {
175
+ script[offset++] = 0x4c // OP_PUSHDATA1
176
+ script[offset++] = totalSize
177
+ } else {
178
+ throw new Error('OP_RETURN data too large for standard push operations')
179
+ }
180
+
181
+ // Add prefix
182
+ prefixBuffer.copy(script, offset)
183
+ offset += prefixBuffer.length
184
+
185
+ // Add message
186
+ msgBuffer.copy(script, offset)
187
+
188
+ return script
189
+ } catch (err) {
190
+ throw new Error(`OP_RETURN script creation failed: ${err.message}`)
191
+ }
192
+ }
193
+
194
+ // Helper methods
195
+
196
+ _selectUtxosForOpReturn (totalOutputAmount, availableUtxos, satsPerByte, numOutputs) {
197
+ try {
198
+ // Filter secure UTXOs (same as send-xec.js)
199
+ let secureUtxos = this.security.filterSecureUtxos(availableUtxos, { excludeDustAttack: false })
200
+
201
+ // If no confirmed UTXOs available, allow unconfirmed ones
202
+ if (secureUtxos.length === 0) {
203
+ console.warn('No confirmed UTXOs available, including unconfirmed UTXOs')
204
+ secureUtxos = this.security.filterSecureUtxos(availableUtxos, { includeUnconfirmed: true, excludeDustAttack: false })
205
+ }
206
+
207
+ if (secureUtxos.length === 0) {
208
+ throw new Error('No spendable UTXOs available')
209
+ }
210
+
211
+ // Sort UTXOs by size (largest first) - using proper value extraction
212
+ const sortedUtxos = secureUtxos.sort((a, b) => {
213
+ return this._getUtxoValue(b) - this._getUtxoValue(a)
214
+ })
215
+
216
+ const selectedUtxos = []
217
+ let totalInputAmount = 0
218
+ let estimatedFee = 0
219
+
220
+ for (const utxo of sortedUtxos) {
221
+ const utxoValue = this._getUtxoValue(utxo)
222
+ selectedUtxos.push(utxo)
223
+ totalInputAmount += utxoValue
224
+
225
+ // Calculate fee including potential change output
226
+ const numInputs = selectedUtxos.length
227
+ const hasChange = (totalInputAmount - totalOutputAmount) > this.dustLimit
228
+ const totalOutputs = numOutputs + (hasChange ? 1 : 0) // +1 for change if needed
229
+
230
+ estimatedFee = this._calculateFee(numInputs, totalOutputs, satsPerByte)
231
+ const totalNeeded = totalOutputAmount + estimatedFee
232
+
233
+ if (totalInputAmount >= totalNeeded) {
234
+ const change = totalInputAmount - totalNeeded
235
+
236
+ return {
237
+ necessaryUtxos: selectedUtxos,
238
+ totalAmount: totalInputAmount,
239
+ estimatedFee,
240
+ change: change > this.dustLimit ? change : 0
241
+ }
242
+ }
243
+ }
244
+
245
+ throw new Error(`Insufficient funds for OP_RETURN transaction. Need: ${totalOutputAmount + estimatedFee}, Available: ${totalInputAmount}`)
246
+ } catch (err) {
247
+ throw new Error(`UTXO selection for OP_RETURN failed: ${err.message}`)
248
+ }
249
+ }
250
+
251
+ _calculateFee (numInputs, numOutputs, satsPerByte) {
252
+ // Estimate transaction size in bytes
253
+ const estimatedSize = (numInputs * 148) + (numOutputs * 34) + 10
254
+ return Math.ceil(estimatedSize * satsPerByte)
255
+ }
256
+
257
+ _createP2PKHScript (hash160) {
258
+ // Create Pay-to-Public-Key-Hash script
259
+ const script = Buffer.alloc(25)
260
+ script[0] = 0x76 // OP_DUP
261
+ script[1] = 0xa9 // OP_HASH160
262
+ script[2] = 0x14 // Push 20 bytes
263
+ hash160.copy(script, 3)
264
+ script[23] = 0x88 // OP_EQUALVERIFY
265
+ script[24] = 0xac // OP_CHECKSIG
266
+ return script
267
+ }
268
+
269
+ _validateMessage (msg) {
270
+ if (msg === null || msg === undefined) {
271
+ return Buffer.alloc(0) // Empty message
272
+ }
273
+
274
+ if (Buffer.isBuffer(msg)) {
275
+ return msg
276
+ }
277
+
278
+ if (typeof msg === 'string') {
279
+ return Buffer.from(msg, 'utf8')
280
+ }
281
+
282
+ throw new Error('Message must be a string or Buffer')
283
+ }
284
+
285
+ _validatePrefix (prefix) {
286
+ if (!prefix) {
287
+ return Buffer.from('6d02', 'hex') // Default memo.cash prefix
288
+ }
289
+
290
+ if (Buffer.isBuffer(prefix)) {
291
+ return prefix
292
+ }
293
+
294
+ if (typeof prefix === 'string') {
295
+ // Assume hex string
296
+ return Buffer.from(prefix, 'hex')
297
+ }
298
+
299
+ throw new Error('Prefix must be a hex string or Buffer')
300
+ }
301
+
302
+ // Same UTXO value extraction as send-xec.js
303
+ _getUtxoValue (utxo) {
304
+ if (utxo.sats !== undefined) {
305
+ return typeof utxo.sats === 'bigint' ? Number(utxo.sats) : parseInt(utxo.sats)
306
+ }
307
+ if (utxo.value !== undefined) {
308
+ return typeof utxo.value === 'bigint' ? Number(utxo.value) : parseInt(utxo.value)
309
+ }
310
+ return 0
311
+ }
312
+ }
313
+
314
+ module.exports = OpReturn
@@ -0,0 +1,270 @@
1
+ /*
2
+ Security validations for minimal XEC wallet
3
+
4
+ Essential security features without over-engineering:
5
+ - Dust attack protection
6
+ - Basic suspicious pattern detection
7
+ - Input validation
8
+ - UTXO safety checks
9
+ */
10
+
11
+ class SecurityValidator {
12
+ constructor (config = {}) {
13
+ // Simple security thresholds - XEC uses 546 satoshis (5.46 XEC) as standard dust limit
14
+ this.dustThreshold = config.dustThreshold || 546
15
+ this.maxTransactionSize = config.maxTransactionSize || 100000 // 100KB
16
+ this.suspiciousPatternThreshold = config.suspiciousPatternThreshold || 10
17
+ this.maxOutputs = config.maxOutputs || 50
18
+ }
19
+
20
+ /**
21
+ * Check if UTXO is safe to spend
22
+ * @param {Object} utxo - UTXO to validate
23
+ * @param {Object} options - Validation options
24
+ * @returns {boolean} - True if safe to spend
25
+ */
26
+ isSecureUtxo (utxo, options = {}) {
27
+ try {
28
+ // Basic structure validation
29
+ if (!this.isValidUtxoStructure(utxo)) {
30
+ return false
31
+ }
32
+
33
+ const satsValue = this._extractSats(utxo)
34
+
35
+ // Dust protection
36
+ if (satsValue < this.dustThreshold) {
37
+ return false
38
+ }
39
+
40
+ // Check confirmation status
41
+ const { includeUnconfirmed = false } = options
42
+ if (!includeUnconfirmed && utxo.blockHeight === -1) {
43
+ return false
44
+ }
45
+
46
+ return true
47
+ } catch (err) {
48
+ console.warn('UTXO security validation failed:', err.message)
49
+ return false
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Detect potential dust attacks in UTXO set
55
+ * @param {Array} utxos - Array of UTXOs to analyze
56
+ * @returns {Object} - Analysis result
57
+ */
58
+ analyzeDustAttack (utxos) {
59
+ const analysis = {
60
+ isDustAttack: false,
61
+ suspiciousUtxos: [],
62
+ duplicateAmounts: new Map(),
63
+ recommendation: 'safe'
64
+ }
65
+
66
+ try {
67
+ // Group by amount
68
+ const amountGroups = new Map()
69
+
70
+ utxos.forEach((utxo, index) => {
71
+ const sats = this._extractSats(utxo)
72
+
73
+ // Only consider small amounts for dust attack analysis
74
+ if (sats < 5000) { // Less than 50 XEC
75
+ if (!amountGroups.has(sats)) {
76
+ amountGroups.set(sats, [])
77
+ }
78
+ amountGroups.get(sats).push({ utxo, index })
79
+ }
80
+ })
81
+
82
+ // Check for suspicious patterns
83
+ for (const [amount, group] of amountGroups) {
84
+ if (group.length >= this.suspiciousPatternThreshold) {
85
+ analysis.isDustAttack = true
86
+ analysis.suspiciousUtxos.push(...group.map(g => g.index))
87
+ analysis.duplicateAmounts.set(amount, group.length)
88
+ }
89
+ }
90
+
91
+ // Set recommendation
92
+ if (analysis.isDustAttack) {
93
+ analysis.recommendation = 'exclude_suspicious'
94
+ }
95
+
96
+ return analysis
97
+ } catch (err) {
98
+ console.warn('Dust attack analysis failed:', err.message)
99
+ return analysis
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Validate transaction outputs for security
105
+ * @param {Array} outputs - Transaction outputs
106
+ * @returns {Object} - Validation result
107
+ */
108
+ validateOutputs (outputs) {
109
+ const validation = {
110
+ isValid: true,
111
+ errors: [],
112
+ totalAmount: 0
113
+ }
114
+
115
+ try {
116
+ // Check output count
117
+ if (outputs.length === 0) {
118
+ validation.isValid = false
119
+ validation.errors.push('No outputs specified')
120
+ return validation
121
+ }
122
+
123
+ if (outputs.length > this.maxOutputs) {
124
+ validation.isValid = false
125
+ validation.errors.push(`Too many outputs: ${outputs.length} > ${this.maxOutputs}`)
126
+ return validation
127
+ }
128
+
129
+ // Validate each output
130
+ for (let i = 0; i < outputs.length; i++) {
131
+ const output = outputs[i]
132
+
133
+ // Address validation
134
+ if (!this.isValidAddress(output.address)) {
135
+ validation.isValid = false
136
+ validation.errors.push(`Invalid address at output ${i}: ${output.address}`)
137
+ }
138
+
139
+ // Amount validation
140
+ const amount = this._validateAmount(output.amountSat || output.amount)
141
+ if (amount === null) {
142
+ validation.isValid = false
143
+ validation.errors.push(`Invalid amount at output ${i}: ${output.amountSat || output.amount}`)
144
+ } else {
145
+ validation.totalAmount += amount
146
+ }
147
+ }
148
+
149
+ // Check for amount overflow
150
+ if (validation.totalAmount > Number.MAX_SAFE_INTEGER) {
151
+ validation.isValid = false
152
+ validation.errors.push('Total amount exceeds safe integer limits')
153
+ }
154
+
155
+ return validation
156
+ } catch (err) {
157
+ validation.isValid = false
158
+ validation.errors.push(`Output validation failed: ${err.message}`)
159
+ return validation
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Check if address is valid XEC format
165
+ * @param {string} address - Address to validate
166
+ * @returns {boolean} - True if valid
167
+ */
168
+ isValidAddress (address) {
169
+ try {
170
+ if (typeof address !== 'string') {
171
+ return false
172
+ }
173
+
174
+ // Must start with ecash: prefix
175
+ if (!address.startsWith('ecash:')) {
176
+ return false
177
+ }
178
+
179
+ // Try to decode with ecashaddrjs for proper validation
180
+ const { decodeCashAddress } = require('ecashaddrjs')
181
+ const decoded = decodeCashAddress(address)
182
+
183
+ // Must be P2PKH (type 0) - case insensitive check
184
+ return decoded.type === 'P2PKH' || decoded.type === 'p2pkh'
185
+ } catch (err) {
186
+ // Invalid address format
187
+ return false
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Validate UTXO structure
193
+ * @param {Object} utxo - UTXO to validate
194
+ * @returns {boolean} - True if valid structure
195
+ */
196
+ isValidUtxoStructure (utxo) {
197
+ return (
198
+ utxo &&
199
+ utxo.outpoint &&
200
+ typeof utxo.outpoint.txid === 'string' &&
201
+ typeof utxo.outpoint.outIdx === 'number' &&
202
+ (utxo.sats !== undefined || utxo.value !== undefined) &&
203
+ (typeof utxo.blockHeight === 'number' || utxo.blockHeight === -1)
204
+ )
205
+ }
206
+
207
+ /**
208
+ * Filter UTXOs to only safe ones
209
+ * @param {Array} utxos - UTXOs to filter
210
+ * @param {Object} options - Filtering options
211
+ * @returns {Array} - Filtered safe UTXOs
212
+ */
213
+ filterSecureUtxos (utxos, options = {}) {
214
+ const {
215
+ includeUnconfirmed = false,
216
+ excludeDustAttack = true
217
+ } = options
218
+
219
+ let filteredUtxos = utxos.filter(utxo => {
220
+ // Basic security check with options
221
+ if (!this.isSecureUtxo(utxo, { includeUnconfirmed })) {
222
+ return false
223
+ }
224
+
225
+ return true
226
+ })
227
+
228
+ // Remove dust attack UTXOs if requested
229
+ if (excludeDustAttack) {
230
+ const dustAnalysis = this.analyzeDustAttack(filteredUtxos)
231
+ if (dustAnalysis.isDustAttack) {
232
+ filteredUtxos = filteredUtxos.filter((_, index) =>
233
+ !dustAnalysis.suspiciousUtxos.includes(index)
234
+ )
235
+ }
236
+ }
237
+
238
+ return filteredUtxos
239
+ }
240
+
241
+ // Private helper methods
242
+
243
+ _extractSats (utxo) {
244
+ if (utxo.sats !== undefined) {
245
+ return typeof utxo.sats === 'bigint' ? Number(utxo.sats) : parseInt(utxo.sats)
246
+ }
247
+ if (utxo.value !== undefined) {
248
+ return typeof utxo.value === 'bigint' ? Number(utxo.value) : parseInt(utxo.value)
249
+ }
250
+ throw new Error('No sats/value found in UTXO')
251
+ }
252
+
253
+ _validateAmount (amount) {
254
+ if (typeof amount === 'string') {
255
+ amount = parseInt(amount)
256
+ }
257
+
258
+ if (typeof amount !== 'number' || isNaN(amount)) {
259
+ return null
260
+ }
261
+
262
+ if (amount <= 0 || amount > Number.MAX_SAFE_INTEGER) {
263
+ return null
264
+ }
265
+
266
+ return amount
267
+ }
268
+ }
269
+
270
+ module.exports = SecurityValidator