@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/solana-api",
3
- "version": "2.4.0",
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.74",
19
- "@exodus/assets-base": "^8.0.157",
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.0",
25
- "@exodus/solana-web3.js": "1.31.0-exodus.5",
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": "301d6e9222b3c0828c73cea460c94c9bbce878e6"
32
+ "gitHead": "a7b27e4f299057c16dda446f006fa61a945a6cbf"
36
33
  }
@@ -1,9 +1,6 @@
1
- import assets from '@exodus/assets'
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, ...solTokens] // deprecated - will be removed
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.tokens[mint] || {
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).unref()
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
- export * from './pay'
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
@@ -1,4 +0,0 @@
1
- export * from './parseURL.js'
2
- export * from './prepareSendData.js'
3
- export * from './validateBeforePay'
4
- export * from './fetchTransaction'
@@ -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
- }