@exodus/solana-api 2.0.14 → 2.1.0

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.0.14",
3
+ "version": "2.1.0",
4
4
  "description": "Exodus internal Solana asset API wrapper",
5
5
  "main": "src/index.js",
6
6
  "files": [
@@ -20,7 +20,8 @@
20
20
  "@exodus/fetch": "^1.2.0",
21
21
  "@exodus/models": "^8.7.2",
22
22
  "@exodus/nfts-core": "^0.5.0",
23
- "@exodus/solana-lib": "^1.3.15",
23
+ "@exodus/solana-lib": "^1.4.0",
24
+ "@ungap/url-search-params": "^0.2.2",
24
25
  "bn.js": "^4.11.0",
25
26
  "debug": "^4.1.1",
26
27
  "lodash": "^4.17.11",
@@ -30,5 +31,5 @@
30
31
  "devDependencies": {
31
32
  "node-fetch": "~2.6.0"
32
33
  },
33
- "gitHead": "16410d60f413eda6aeb3372c23875b98c20de06c"
34
+ "gitHead": "6ffded97383f994c21ccaebb2703c32d48aa70c5"
34
35
  }
package/src/api.js CHANGED
@@ -101,8 +101,12 @@ export class Api {
101
101
  return this.api.post({ method, params })
102
102
  }
103
103
 
104
+ getTokenByAddress(mint: string) {
105
+ return this.tokens[mint]
106
+ }
107
+
104
108
  isTokenSupported(mint: string) {
105
- return !!this.tokens[mint]
109
+ return !!this.getTokenByAddress(mint)
106
110
  }
107
111
 
108
112
  async getEpochInfo(): number {
@@ -115,10 +119,10 @@ export class Api {
115
119
  return state
116
120
  }
117
121
 
118
- async getRecentBlockHash(): string {
122
+ async getRecentBlockHash(commitment?: string): Promise<string> {
119
123
  const result = await this.rpcCall(
120
124
  'getRecentBlockhash',
121
- [{ commitment: 'finalized', encoding: 'jsonParsed' }],
125
+ [{ commitment: commitment || 'finalized', encoding: 'jsonParsed' }],
122
126
  { forceHttp: true }
123
127
  )
124
128
  return lodash.get(result, 'value.blockhash')
@@ -234,14 +238,8 @@ export class Api {
234
238
  tokenAccountsByOwner: ?Array,
235
239
  { includeUnparsed = false } = {}
236
240
  ): Object {
237
- let {
238
- fee,
239
- preBalances,
240
- postBalances,
241
- preTokenBalances,
242
- postTokenBalances,
243
- innerInstructions,
244
- } = txDetails.meta
241
+ let { fee, preBalances, postBalances, preTokenBalances, postTokenBalances, innerInstructions } =
242
+ txDetails.meta
245
243
  preBalances = preBalances || []
246
244
  postBalances = postBalances || []
247
245
  preTokenBalances = preTokenBalances || []
package/src/index.js CHANGED
@@ -2,6 +2,7 @@ import { Api } from './api'
2
2
  export * from './api'
3
3
  export * from './tx-log'
4
4
  export * from './account-state'
5
+ export * from './pay'
5
6
 
6
7
  // At some point we would like to exclude this export. Default export should be the whole asset "plugin" ready to be injected.
7
8
  // Clients should not call an specific server api directly.
@@ -0,0 +1,3 @@
1
+ export * from './parseURL.js'
2
+ export * from './prepareSendData.js'
3
+ export * from './validateBeforePay'
@@ -0,0 +1,117 @@
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
+ }
@@ -0,0 +1,26 @@
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
+ }
@@ -0,0 +1,37 @@
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
+ }