@exodus/solana-api 2.1.0 → 2.1.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 -3
- package/src/api.js +18 -9
- package/src/pay/fetchTransaction.js +139 -0
- package/src/pay/index.js +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/solana-api",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.2",
|
|
4
4
|
"description": "Exodus internal Solana asset API wrapper",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"files": [
|
|
@@ -20,16 +20,19 @@
|
|
|
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.4.
|
|
23
|
+
"@exodus/solana-lib": "^1.4.2",
|
|
24
|
+
"@exodus/solana-web3.js": "1.31.0-exodus.3",
|
|
24
25
|
"@ungap/url-search-params": "^0.2.2",
|
|
26
|
+
"bignumber.js": "^9.0.1",
|
|
25
27
|
"bn.js": "^4.11.0",
|
|
26
28
|
"debug": "^4.1.1",
|
|
27
29
|
"lodash": "^4.17.11",
|
|
30
|
+
"tweetnacl": "^1.0.3",
|
|
28
31
|
"url-join": "4.0.0",
|
|
29
32
|
"wretch": "^1.5.2"
|
|
30
33
|
},
|
|
31
34
|
"devDependencies": {
|
|
32
35
|
"node-fetch": "~2.6.0"
|
|
33
36
|
},
|
|
34
|
-
"gitHead": "
|
|
37
|
+
"gitHead": "1da3192650571fda10b61596dbc2eaf7cc8f439d"
|
|
35
38
|
}
|
package/src/api.js
CHANGED
|
@@ -136,7 +136,7 @@ export class Api {
|
|
|
136
136
|
])
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
async getFee(): number {
|
|
139
|
+
async getFee(): Promise<number> {
|
|
140
140
|
const result = await this.rpcCall('getRecentBlockhash', [
|
|
141
141
|
{ commitment: 'finalized', encoding: 'jsonParsed' },
|
|
142
142
|
])
|
|
@@ -238,8 +238,14 @@ export class Api {
|
|
|
238
238
|
tokenAccountsByOwner: ?Array,
|
|
239
239
|
{ includeUnparsed = false } = {}
|
|
240
240
|
): Object {
|
|
241
|
-
let {
|
|
242
|
-
|
|
241
|
+
let {
|
|
242
|
+
fee,
|
|
243
|
+
preBalances,
|
|
244
|
+
postBalances,
|
|
245
|
+
preTokenBalances,
|
|
246
|
+
postTokenBalances,
|
|
247
|
+
innerInstructions,
|
|
248
|
+
} = txDetails.meta
|
|
243
249
|
preBalances = preBalances || []
|
|
244
250
|
postBalances = postBalances || []
|
|
245
251
|
preTokenBalances = preTokenBalances || []
|
|
@@ -584,7 +590,7 @@ export class Api {
|
|
|
584
590
|
return tokensMint
|
|
585
591
|
}
|
|
586
592
|
|
|
587
|
-
async getTokenAccountsByOwner(address: string, tokenTicker: ?string): Array {
|
|
593
|
+
async getTokenAccountsByOwner(address: string, tokenTicker: ?string): Promise<Array> {
|
|
588
594
|
const { value: accountsList } = await this.rpcCall(
|
|
589
595
|
'getTokenAccountsByOwner',
|
|
590
596
|
[address, { programId: TOKEN_PROGRAM_ID.toBase58() }, { encoding: 'jsonParsed' }],
|
|
@@ -926,13 +932,16 @@ export class Api {
|
|
|
926
932
|
* Simulate transaction and return side effects
|
|
927
933
|
*/
|
|
928
934
|
simulateAndRetrieveSideEffects = async (
|
|
929
|
-
|
|
930
|
-
publicKey: string
|
|
935
|
+
message: SolanaWeb3Message,
|
|
936
|
+
publicKey: string,
|
|
937
|
+
transactionMessage?: any // decompiled TransactionMessage
|
|
931
938
|
) => {
|
|
932
|
-
const { config, accountAddresses } = getTransactionSimulationParams(
|
|
933
|
-
|
|
939
|
+
const { config, accountAddresses } = getTransactionSimulationParams(
|
|
940
|
+
transactionMessage || message
|
|
941
|
+
)
|
|
942
|
+
const signatures = new Array(message.header.numRequiredSignatures || 1).fill(null)
|
|
934
943
|
const encodedTransaction = buildRawTransaction(
|
|
935
|
-
|
|
944
|
+
Buffer.from(message.serialize()),
|
|
936
945
|
signatures
|
|
937
946
|
).toString('base64')
|
|
938
947
|
const futureAccountsState = await this.simulateTransaction(encodedTransaction, config)
|
|
@@ -0,0 +1,139 @@
|
|
|
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
CHANGED