@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 +4 -3
- package/src/api.js +9 -11
- package/src/index.js +1 -0
- package/src/pay/index.js +3 -0
- package/src/pay/parseURL.js +117 -0
- package/src/pay/prepareSendData.js +26 -0
- package/src/pay/validateBeforePay.js +37 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/solana-api",
|
|
3
|
-
"version": "2.0
|
|
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.
|
|
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": "
|
|
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.
|
|
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
|
-
|
|
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.
|
package/src/pay/index.js
ADDED
|
@@ -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
|
+
}
|