@exodus/solana-api 2.4.0 → 2.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -9
- package/src/account-state.js +2 -5
- package/src/api.js +2 -3
- package/src/connection.js +2 -1
- package/src/index.js +10 -2
- package/src/pay/fetchTransaction.js +0 -139
- package/src/pay/index.js +0 -4
- package/src/pay/parseURL.js +0 -117
- package/src/pay/prepareSendData.js +0 -26
- package/src/pay/validateBeforePay.js +0 -37
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/solana-api",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.2",
|
|
4
4
|
"description": "Exodus internal Solana asset API wrapper",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"files": [
|
|
@@ -15,22 +15,19 @@
|
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"@exodus/asset-json-rpc": "^1.0.0",
|
|
17
17
|
"@exodus/asset-lib": "^3.7.1",
|
|
18
|
-
"@exodus/assets": "^8.0.
|
|
19
|
-
"@exodus/
|
|
18
|
+
"@exodus/assets": "^8.0.85",
|
|
19
|
+
"@exodus/basic-utils": "^1.3.0",
|
|
20
20
|
"@exodus/fetch": "^1.2.0",
|
|
21
21
|
"@exodus/models": "^8.10.4",
|
|
22
22
|
"@exodus/nfts-core": "^0.5.0",
|
|
23
23
|
"@exodus/simple-retry": "^0.0.6",
|
|
24
|
-
"@exodus/solana-lib": "^1.6.
|
|
25
|
-
"@exodus/solana-
|
|
26
|
-
"@ungap/url-search-params": "^0.2.2",
|
|
27
|
-
"bignumber.js": "^9.0.1",
|
|
24
|
+
"@exodus/solana-lib": "^1.6.3",
|
|
25
|
+
"@exodus/solana-meta": "^1.0.2",
|
|
28
26
|
"bn.js": "^4.11.0",
|
|
29
27
|
"debug": "^4.1.1",
|
|
30
28
|
"lodash": "^4.17.11",
|
|
31
|
-
"tweetnacl": "^1.0.3",
|
|
32
29
|
"url-join": "4.0.0",
|
|
33
30
|
"wretch": "^1.5.2"
|
|
34
31
|
},
|
|
35
|
-
"gitHead": "
|
|
32
|
+
"gitHead": "a7b27e4f299057c16dda446f006fa61a945a6cbf"
|
|
36
33
|
}
|
package/src/account-state.js
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { asset, tokens } from '@exodus/solana-meta'
|
|
2
2
|
import { AccountState } from '@exodus/models'
|
|
3
3
|
|
|
4
|
-
const asset = assets.solana
|
|
5
|
-
const solTokens = Object.values(assets).filter((asset) => asset.assetType === 'SOLANA_TOKEN')
|
|
6
|
-
|
|
7
4
|
export class SolanaAccountState extends AccountState {
|
|
8
5
|
static defaults = {
|
|
9
6
|
cursor: '',
|
|
@@ -24,7 +21,7 @@ export class SolanaAccountState extends AccountState {
|
|
|
24
21
|
accounts: {}, // stake accounts
|
|
25
22
|
},
|
|
26
23
|
}
|
|
27
|
-
static _tokens = [asset, ...
|
|
24
|
+
static _tokens = [asset, ...tokens] // deprecated - will be removed
|
|
28
25
|
|
|
29
26
|
static _postParse(data) {
|
|
30
27
|
return { ...data, tokenBalances: {} }
|
package/src/api.js
CHANGED
|
@@ -15,7 +15,6 @@ import {
|
|
|
15
15
|
SolanaWeb3Message,
|
|
16
16
|
buildRawTransaction,
|
|
17
17
|
} from '@exodus/solana-lib'
|
|
18
|
-
import assets from '@exodus/assets'
|
|
19
18
|
import assert from 'assert'
|
|
20
19
|
import lodash from 'lodash'
|
|
21
20
|
import urljoin from 'url-join'
|
|
@@ -30,7 +29,7 @@ const WS_ENDPOINT = 'wss://solana.a.exodus.io/ws'
|
|
|
30
29
|
|
|
31
30
|
// Tokens + SOL api support
|
|
32
31
|
export class Api {
|
|
33
|
-
constructor(rpcUrl, wsUrl) {
|
|
32
|
+
constructor({ rpcUrl, wsUrl, assets }) {
|
|
34
33
|
this.setServer(rpcUrl)
|
|
35
34
|
this.setWsEndpoint(wsUrl)
|
|
36
35
|
this.setTokens(assets)
|
|
@@ -603,7 +602,7 @@ export class Api {
|
|
|
603
602
|
const { pubkey, account } = entry
|
|
604
603
|
|
|
605
604
|
const mint = lodash.get(account, 'data.parsed.info.mint')
|
|
606
|
-
const token = this.
|
|
605
|
+
const token = this.getTokenByAddress(mint) || {
|
|
607
606
|
name: 'unknown',
|
|
608
607
|
ticker: 'UNKNOWN',
|
|
609
608
|
}
|
package/src/connection.js
CHANGED
|
@@ -186,7 +186,8 @@ export class Connection {
|
|
|
186
186
|
delete this.rpcQueue[id]
|
|
187
187
|
debug(`ws timeout command: ${method} - ${JSON.stringify(params)} - ${id}`)
|
|
188
188
|
reject(new Error('solana ws: reply timeout'))
|
|
189
|
-
}, TIMEOUT)
|
|
189
|
+
}, TIMEOUT)
|
|
190
|
+
if (typeof this.rpcQueue[id].timeout.unref === 'function') this.rpcQueue[id].timeout.unref()
|
|
190
191
|
this.ws.send(JSON.stringify({ jsonrpc: '2.0', method, params, id }))
|
|
191
192
|
})
|
|
192
193
|
}
|
package/src/index.js
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
|
+
import { keyBy } from '@exodus/basic-utils'
|
|
2
|
+
import { connectAssets } from '@exodus/assets'
|
|
3
|
+
import assetsList from '@exodus/solana-meta'
|
|
4
|
+
|
|
1
5
|
import { Api } from './api'
|
|
6
|
+
|
|
2
7
|
export * from './api'
|
|
3
8
|
export * from './tx-log'
|
|
4
9
|
export * from './account-state'
|
|
5
|
-
|
|
10
|
+
|
|
11
|
+
// These are not the same asset objects as the wallet creates, so they should never be returned to the wallet.
|
|
12
|
+
// Initially this may be violated by the Solana code until the first monitor tick updates assets with setTokens()
|
|
13
|
+
const assets = connectAssets(keyBy(assetsList, (asset) => asset.name))
|
|
6
14
|
|
|
7
15
|
// At some point we would like to exclude this export. Default export should be the whole asset "plugin" ready to be injected.
|
|
8
16
|
// Clients should not call an specific server api directly.
|
|
9
|
-
const serverApi = new Api()
|
|
17
|
+
const serverApi = new Api({ assets })
|
|
10
18
|
export default serverApi
|
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
// @flow
|
|
2
|
-
import nacl from 'tweetnacl'
|
|
3
|
-
import { fetchival } from '@exodus/fetch'
|
|
4
|
-
import ms from 'ms'
|
|
5
|
-
import { SystemInstruction, Transaction, PublicKey } from '@exodus/solana-web3.js'
|
|
6
|
-
import api from '..'
|
|
7
|
-
import BigNumber from 'bignumber.js'
|
|
8
|
-
import {
|
|
9
|
-
TOKEN_PROGRAM_ID,
|
|
10
|
-
decodeTokenProgramInstruction,
|
|
11
|
-
SYSTEM_PROGRAM_ID,
|
|
12
|
-
} from '@exodus/solana-lib'
|
|
13
|
-
|
|
14
|
-
export class FetchTransactionError extends Error {
|
|
15
|
-
name = 'FetchTransactionError'
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export class ParseTransactionError extends Error {
|
|
19
|
-
name = 'ParseTransactionError'
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const isTransferCheckedInstruction = (decodedInstruction) =>
|
|
23
|
-
decodedInstruction.type === 'transferChecked'
|
|
24
|
-
const isTransferInstruction = (decodedInstruction) => decodedInstruction.type === 'transfer'
|
|
25
|
-
const isSplAccount = (account) =>
|
|
26
|
-
account && account.owner === 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'
|
|
27
|
-
|
|
28
|
-
export async function fetchTransaction({
|
|
29
|
-
account,
|
|
30
|
-
link,
|
|
31
|
-
commitment,
|
|
32
|
-
}: {
|
|
33
|
-
account: string,
|
|
34
|
-
link: string | URL,
|
|
35
|
-
commitment?: string,
|
|
36
|
-
}) {
|
|
37
|
-
const response = await fetchival(String(link), {
|
|
38
|
-
mode: 'cors',
|
|
39
|
-
cache: 'no-cache',
|
|
40
|
-
credentials: 'omit',
|
|
41
|
-
timeout: ms('10s'),
|
|
42
|
-
headers: {
|
|
43
|
-
Accept: 'application/json',
|
|
44
|
-
'Content-Type': 'application/json',
|
|
45
|
-
},
|
|
46
|
-
}).post({ account })
|
|
47
|
-
if (!response || !response.transaction) throw new FetchTransactionError('missing transaction')
|
|
48
|
-
const { transaction: txString } = response
|
|
49
|
-
if (typeof txString !== 'string') throw new FetchTransactionError('invalid transaction')
|
|
50
|
-
const transaction = Transaction.from(Buffer.from(txString, 'base64'))
|
|
51
|
-
|
|
52
|
-
const { signatures, feePayer, recentBlockhash } = transaction
|
|
53
|
-
if (signatures.length) {
|
|
54
|
-
if (!feePayer) throw new FetchTransactionError('missing fee payer')
|
|
55
|
-
if (!feePayer.equals(signatures[0].publicKey))
|
|
56
|
-
throw new FetchTransactionError('invalid fee payer')
|
|
57
|
-
if (!recentBlockhash) throw new FetchTransactionError('missing recent blockhash')
|
|
58
|
-
|
|
59
|
-
// A valid signature for everything except `account` must be provided.
|
|
60
|
-
const message = transaction.serializeMessage()
|
|
61
|
-
for (const { signature, publicKey } of signatures) {
|
|
62
|
-
if (signature) {
|
|
63
|
-
if (!nacl.sign.detached.verify(message, signature, publicKey.toBuffer()))
|
|
64
|
-
throw new FetchTransactionError('invalid signature')
|
|
65
|
-
} else if (publicKey.equals(new PublicKey(account))) {
|
|
66
|
-
// If the only signature expected is for `account`, ignore the recent blockhash in the transaction.
|
|
67
|
-
if (signatures.length === 1) {
|
|
68
|
-
transaction.recentBlockhash = await api.getRecentBlockHash(commitment)
|
|
69
|
-
}
|
|
70
|
-
} else {
|
|
71
|
-
throw new FetchTransactionError('missing signature')
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
} else {
|
|
75
|
-
// Ignore the fee payer and recent blockhash in the transaction and initialize them.
|
|
76
|
-
transaction.feePayer = account
|
|
77
|
-
transaction.recentBlockhash = await api.getRecentBlockHash(commitment)
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return parseInstructions(transaction)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
async function parseInstructions(transaction: Transaction) {
|
|
84
|
-
// Make a copy of the instructions we're going to mutate it.
|
|
85
|
-
const instructions = transaction.instructions.slice()
|
|
86
|
-
|
|
87
|
-
if (!instructions || !Array.isArray(instructions) || instructions.length !== 1)
|
|
88
|
-
throw new ParseTransactionError('Invalid transaction instructions')
|
|
89
|
-
|
|
90
|
-
// Transfer instruction must be the last instruction
|
|
91
|
-
const instruction = instructions.pop()
|
|
92
|
-
if (!instruction) throw new ParseTransactionError('missing transfer instruction')
|
|
93
|
-
|
|
94
|
-
const isTokenTransfer = instruction.programId.equals(TOKEN_PROGRAM_ID)
|
|
95
|
-
const isSolNativeTransfer = instruction.programId.equals(SYSTEM_PROGRAM_ID)
|
|
96
|
-
|
|
97
|
-
if (isTokenTransfer) {
|
|
98
|
-
const decodedInstruction = decodeTokenProgramInstruction(instruction)
|
|
99
|
-
if (
|
|
100
|
-
!isTransferCheckedInstruction(decodedInstruction) &&
|
|
101
|
-
!isTransferInstruction(decodedInstruction)
|
|
102
|
-
)
|
|
103
|
-
throw new ParseTransactionError('invalid token transfer')
|
|
104
|
-
|
|
105
|
-
const [, mint, destination, owner] = instruction.keys
|
|
106
|
-
|
|
107
|
-
const splToken = mint.pubkey.toBase58()
|
|
108
|
-
let asset
|
|
109
|
-
let recipient = destination.pubkey.toBase58()
|
|
110
|
-
if (splToken) {
|
|
111
|
-
if (!api.isTokenSupported(splToken))
|
|
112
|
-
throw new ParseTransactionError(`spl-token ${splToken} is not supported`)
|
|
113
|
-
asset = api.getTokenByAddress(splToken)
|
|
114
|
-
|
|
115
|
-
const account = await api.getAccountInfo(recipient)
|
|
116
|
-
if (isSplAccount(account)) recipient = account.data.parsed.info.owner
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
return {
|
|
120
|
-
amount: asset.currency
|
|
121
|
-
.baseUnit(new BigNumber(decodedInstruction.data.amount).toString())
|
|
122
|
-
.toDefaultString(),
|
|
123
|
-
decimals: decodedInstruction.data.decimals,
|
|
124
|
-
recipient,
|
|
125
|
-
sender: owner.pubkey.toBase58(),
|
|
126
|
-
splToken,
|
|
127
|
-
asset,
|
|
128
|
-
}
|
|
129
|
-
} else if (isSolNativeTransfer) {
|
|
130
|
-
const decodedTransaction = SystemInstruction.decodeTransfer(instruction)
|
|
131
|
-
return {
|
|
132
|
-
sender: decodedTransaction.fromPubkey.toString(),
|
|
133
|
-
amount: decodedTransaction.lamports.toString(),
|
|
134
|
-
recipient: decodedTransaction.toPubkey.toString(),
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
throw new Error('Invalid transfer instruction')
|
|
139
|
-
}
|
package/src/pay/index.js
DELETED
package/src/pay/parseURL.js
DELETED
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
// @flow
|
|
2
|
-
import { PublicKey } from '@exodus/solana-lib'
|
|
3
|
-
import BigNumber from 'bignumber.js'
|
|
4
|
-
import assets from '@exodus/assets'
|
|
5
|
-
import URLSearchParams from '@ungap/url-search-params'
|
|
6
|
-
import api from '..'
|
|
7
|
-
|
|
8
|
-
const SOLANA_PROTOCOL = 'solana:'
|
|
9
|
-
const HTTPS_PROTOCOL = 'https:'
|
|
10
|
-
|
|
11
|
-
export class ParseURLError extends Error {
|
|
12
|
-
name = 'ParseURLError'
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export type TransactionRequestURL = {
|
|
16
|
-
link: URL,
|
|
17
|
-
label?: string,
|
|
18
|
-
message?: string,
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export type TransferRequestURL = {
|
|
22
|
-
recipient: string,
|
|
23
|
-
reference?: Array<string>,
|
|
24
|
-
amount?: string,
|
|
25
|
-
splToken?: string,
|
|
26
|
-
message?: string,
|
|
27
|
-
memo?: string,
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export type ParsedURL = TransactionRequestURL | TransferRequestURL
|
|
31
|
-
|
|
32
|
-
export function parseURL(url: string | URL): ParsedURL {
|
|
33
|
-
if (typeof url === 'string') {
|
|
34
|
-
if (url.length > 2048) throw new ParseURLError('length invalid')
|
|
35
|
-
url = new URL(url)
|
|
36
|
-
}
|
|
37
|
-
if (url.protocol !== SOLANA_PROTOCOL) throw new ParseURLError('protocol invalid')
|
|
38
|
-
if (!url.pathname) throw new ParseURLError('pathname missing')
|
|
39
|
-
return /[:%]/.test(url.pathname) ? parseTransactionRequestURL(url) : parseTransferRequestURL(url)
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function parseTransactionRequestURL(url: URL): TransactionRequestURL {
|
|
43
|
-
const link = new URL(decodeURIComponent(url.pathname))
|
|
44
|
-
const searchParams = new URLSearchParams(url.search)
|
|
45
|
-
if (link.protocol !== HTTPS_PROTOCOL) throw new ParseURLError('link invalid')
|
|
46
|
-
|
|
47
|
-
const label = searchParams.get('label') || undefined
|
|
48
|
-
const message = searchParams.get('message') || undefined
|
|
49
|
-
|
|
50
|
-
return {
|
|
51
|
-
link,
|
|
52
|
-
label,
|
|
53
|
-
message,
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function parseTransferRequestURL(url: URL): TransferRequestURL {
|
|
58
|
-
let recipient: PublicKey
|
|
59
|
-
try {
|
|
60
|
-
recipient = new PublicKey(url.pathname)
|
|
61
|
-
} catch (error) {
|
|
62
|
-
throw new Error('ParseURLError: recipient invalid')
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const searchParams = new URLSearchParams(url.search)
|
|
66
|
-
|
|
67
|
-
let amount: BigNumber
|
|
68
|
-
const amountParam = searchParams.get('amount')
|
|
69
|
-
if (amountParam) {
|
|
70
|
-
if (!/^\d+(\.\d+)?$/.test(amountParam)) throw new Error('ParseURLError: amount invalid')
|
|
71
|
-
amount = new BigNumber(amountParam)
|
|
72
|
-
if (amount.isNaN()) throw new Error('ParseURLError: amount NaN')
|
|
73
|
-
if (amount.isNegative()) throw new Error('ParseURLError: amount negative')
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
let splToken: PublicKey
|
|
77
|
-
const splTokenParam = searchParams.get('spl-token')
|
|
78
|
-
if (splTokenParam) {
|
|
79
|
-
try {
|
|
80
|
-
splToken = new PublicKey(splTokenParam)
|
|
81
|
-
} catch (error) {
|
|
82
|
-
throw new ParseURLError('spl-token invalid')
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
let asset = assets.solana
|
|
87
|
-
if (splToken) {
|
|
88
|
-
if (!api.isTokenSupported(splToken))
|
|
89
|
-
throw new ParseURLError(`spl-token ${splToken} is not supported`)
|
|
90
|
-
asset = api.getTokenByAddress(splToken)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
let reference: PublicKey[]
|
|
94
|
-
const referenceParams = searchParams.getAll('reference')
|
|
95
|
-
if (referenceParams.length) {
|
|
96
|
-
try {
|
|
97
|
-
reference = referenceParams.map((reference) => new PublicKey(reference))
|
|
98
|
-
} catch (error) {
|
|
99
|
-
throw new ParseURLError('reference invalid')
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const label = searchParams.get('label') || undefined
|
|
104
|
-
const message = searchParams.get('message') || undefined
|
|
105
|
-
const memo = searchParams.get('memo') || undefined
|
|
106
|
-
|
|
107
|
-
return {
|
|
108
|
-
asset,
|
|
109
|
-
recipient: recipient.toString(),
|
|
110
|
-
amount: amount ? amount.toString(10) : undefined,
|
|
111
|
-
splToken: splToken ? splToken.toString() : undefined,
|
|
112
|
-
reference,
|
|
113
|
-
label,
|
|
114
|
-
message,
|
|
115
|
-
memo,
|
|
116
|
-
}
|
|
117
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
// @flow
|
|
2
|
-
import assert from 'assert'
|
|
3
|
-
|
|
4
|
-
import type { ParsedURL } from './parseURL'
|
|
5
|
-
|
|
6
|
-
export function prepareSendData(parsedData: ParsedURL, { asset, feeAmount, walletAccount }) {
|
|
7
|
-
const { amount: amountStr, recipient, splToken, ...options } = parsedData
|
|
8
|
-
|
|
9
|
-
assert(amountStr, 'PrepareTxError: Missing amount')
|
|
10
|
-
assert(recipient, 'PrepareTxError: Missing recipient')
|
|
11
|
-
|
|
12
|
-
const amount = asset.currency.defaultUnit(amountStr)
|
|
13
|
-
|
|
14
|
-
return {
|
|
15
|
-
asset: asset.name,
|
|
16
|
-
baseAsset: asset.baseAsset,
|
|
17
|
-
customMintAddress: splToken,
|
|
18
|
-
feeAmount,
|
|
19
|
-
receiver: {
|
|
20
|
-
address: recipient,
|
|
21
|
-
amount,
|
|
22
|
-
},
|
|
23
|
-
walletAccount,
|
|
24
|
-
...options,
|
|
25
|
-
}
|
|
26
|
-
}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
// @flow
|
|
2
|
-
import api from '..'
|
|
3
|
-
import NumberUnit from '@exodus/currency'
|
|
4
|
-
|
|
5
|
-
export class ValidateError extends Error {
|
|
6
|
-
name = 'ValidateError'
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
type PaymentRequest = {
|
|
10
|
-
asset: Object,
|
|
11
|
-
senderInfo: Object,
|
|
12
|
-
feeAmount: NumberUnit,
|
|
13
|
-
recipient: string,
|
|
14
|
-
amount: number,
|
|
15
|
-
checkEnoughBalance?: boolean,
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export async function validateBeforePay({
|
|
19
|
-
asset,
|
|
20
|
-
senderInfo,
|
|
21
|
-
amount,
|
|
22
|
-
feeAmount = 0,
|
|
23
|
-
recipient,
|
|
24
|
-
checkEnoughBalance = true,
|
|
25
|
-
}: PaymentRequest) {
|
|
26
|
-
const isNative = asset.name === asset.baseAsset.name
|
|
27
|
-
const recipientInfo = await api.getAccountInfo(recipient)
|
|
28
|
-
if (!recipientInfo) throw new ValidateError(`recipient ${recipient} not found`)
|
|
29
|
-
|
|
30
|
-
if (checkEnoughBalance) {
|
|
31
|
-
const totalAmountMustPay = asset.currency.defaultUnit(amount).add(feeAmount)
|
|
32
|
-
const currentBalance = isNative ? senderInfo.balance : senderInfo.tokenBalances[asset.name]
|
|
33
|
-
if (totalAmountMustPay.gt(currentBalance)) throw new ValidateError(`insufficient funds`)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return true
|
|
37
|
-
}
|