@exodus/solana-api 2.5.16 → 2.5.17

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.5.16",
3
+ "version": "2.5.17",
4
4
  "description": "Exodus internal Solana asset API wrapper",
5
5
  "main": "src/index.js",
6
6
  "files": [
@@ -16,14 +16,14 @@
16
16
  },
17
17
  "dependencies": {
18
18
  "@exodus/asset-json-rpc": "^1.0.0",
19
- "@exodus/asset-lib": "^3.7.1",
20
- "@exodus/assets": "^8.0.85",
21
- "@exodus/basic-utils": "^1.3.0",
19
+ "@exodus/asset-lib": "^4.0.0",
20
+ "@exodus/assets": "^9.0.1",
21
+ "@exodus/basic-utils": "^2.1.0",
22
22
  "@exodus/fetch": "^1.2.0",
23
- "@exodus/models": "^8.10.4",
23
+ "@exodus/models": "^10.1.0",
24
24
  "@exodus/nfts-core": "^0.5.0",
25
25
  "@exodus/simple-retry": "^0.0.6",
26
- "@exodus/solana-lib": "^1.6.7",
26
+ "@exodus/solana-lib": "^1.6.8",
27
27
  "@exodus/solana-meta": "^1.0.3",
28
28
  "bn.js": "^4.11.0",
29
29
  "debug": "^4.1.1",
@@ -31,5 +31,8 @@
31
31
  "url-join": "4.0.0",
32
32
  "wretch": "^1.5.2"
33
33
  },
34
- "gitHead": "9ec7084bcdfe14f1c6f11df393351885773046a6"
34
+ "devDependencies": {
35
+ "@exodus/assets-testing": "file:../../../__testing__"
36
+ },
37
+ "gitHead": "846ed76714355879216f43c6be8ec2b072169635"
35
38
  }
package/src/api.js CHANGED
@@ -1,4 +1,3 @@
1
- // @flow
2
1
  import BN from 'bn.js'
3
2
  import createApi from '@exodus/asset-json-rpc'
4
3
  import { retry } from '@exodus/simple-retry'
@@ -12,13 +11,12 @@ import {
12
11
  TOKEN_PROGRAM_ID,
13
12
  SOL_DECIMAL,
14
13
  computeBalance,
15
- SolanaWeb3Message,
16
14
  buildRawTransaction,
17
15
  } from '@exodus/solana-lib'
18
16
  import assert from 'assert'
19
17
  import lodash from 'lodash'
20
18
  import urljoin from 'url-join'
21
- import wretch, { Wretcher } from 'wretch'
19
+ import wretch from 'wretch'
22
20
  import { magicEden } from '@exodus/nfts-core'
23
21
  import { Connection } from './connection'
24
22
 
@@ -51,7 +49,7 @@ export class Api {
51
49
  this.tokens = new Map(Object.values(solTokens).map((v) => [v.mintAddress, v]))
52
50
  }
53
51
 
54
- request(path, contentType = 'application/json'): Wretcher {
52
+ request(path, contentType = 'application/json') {
55
53
  return wretch(urljoin(this.rpcUrl, path)).headers({
56
54
  'Content-type': contentType,
57
55
  })
@@ -101,25 +99,25 @@ export class Api {
101
99
  return this.api.post({ method, params })
102
100
  }
103
101
 
104
- getTokenByAddress(mint: string) {
102
+ getTokenByAddress(mint) {
105
103
  return this.tokens.get(mint)
106
104
  }
107
105
 
108
- isTokenSupported(mint: string) {
106
+ isTokenSupported(mint) {
109
107
  return this.tokens.has(mint)
110
108
  }
111
109
 
112
- async getEpochInfo(): number {
110
+ async getEpochInfo() {
113
111
  const { epoch } = await this.rpcCall('getEpochInfo')
114
112
  return Number(epoch)
115
113
  }
116
114
 
117
- async getStakeActivation(address): string {
115
+ async getStakeActivation(address) {
118
116
  const { state } = await this.rpcCall('getStakeActivation', [address])
119
117
  return state
120
118
  }
121
119
 
122
- async getRecentBlockHash(commitment?: string): Promise<string> {
120
+ async getRecentBlockHash(commitment?) {
123
121
  const result = await this.rpcCall(
124
122
  'getRecentBlockhash',
125
123
  [{ commitment: commitment || 'finalized', encoding: 'jsonParsed' }],
@@ -129,33 +127,33 @@ export class Api {
129
127
  }
130
128
 
131
129
  // Transaction structure: https://docs.solana.com/apps/jsonrpc-api#transaction-structure
132
- async getTransactionById(id: string) {
130
+ async getTransactionById(id) {
133
131
  return this.rpcCall('getTransaction', [
134
132
  id,
135
133
  { encoding: 'jsonParsed', maxSupportedTransactionVersion: 0 },
136
134
  ])
137
135
  }
138
136
 
139
- async getFee(): Promise<number> {
137
+ async getFee() {
140
138
  const result = await this.rpcCall('getRecentBlockhash', [
141
139
  { commitment: 'finalized', encoding: 'jsonParsed' },
142
140
  ])
143
141
  return lodash.get(result, 'value.feeCalculator.lamportsPerSignature')
144
142
  }
145
143
 
146
- async getBalance(address: string): number {
144
+ async getBalance(address) {
147
145
  const result = await this.rpcCall('getBalance', [address, { encoding: 'jsonParsed' }], {
148
146
  address,
149
147
  })
150
148
  return lodash.get(result, 'value', 0)
151
149
  }
152
150
 
153
- async getBlockTime(slot: number) {
151
+ async getBlockTime(slot) {
154
152
  // might result in error if executed on a validator with partial ledger (https://github.com/solana-labs/solana/issues/12413)
155
153
  return this.rpcCall('getBlockTime', [slot])
156
154
  }
157
155
 
158
- async getConfirmedSignaturesForAddress(address: string, { until, before, limit } = {}): any {
156
+ async getConfirmedSignaturesForAddress(address, { until, before, limit } = {}) {
159
157
  until = until || undefined
160
158
  return this.rpcCall('getSignaturesForAddress', [address, { until, before, limit }], { address })
161
159
  }
@@ -163,10 +161,7 @@ export class Api {
163
161
  /**
164
162
  * Get transactions from an address
165
163
  */
166
- async getTransactions(
167
- address: string,
168
- { cursor, before, limit, includeUnparsed = false } = {}
169
- ): any {
164
+ async getTransactions(address, { cursor, before, limit, includeUnparsed = false } = {}) {
170
165
  let transactions = []
171
166
  // cursor is a txHash
172
167
 
@@ -233,11 +228,11 @@ export class Api {
233
228
  }
234
229
 
235
230
  parseTransaction(
236
- ownerAddress: string,
237
- txDetails: Object,
238
- tokenAccountsByOwner: ?Array,
231
+ ownerAddress,
232
+ txDetails,
233
+ tokenAccountsByOwner,
239
234
  { includeUnparsed = false } = {}
240
- ): Object {
235
+ ) {
241
236
  let {
242
237
  fee,
243
238
  preBalances,
@@ -563,7 +558,7 @@ export class Api {
563
558
  }
564
559
  }
565
560
 
566
- async getSupply(mintAddress: string): string {
561
+ async getSupply(mintAddress) {
567
562
  const result = await this.rpcCall('getTokenSupply', [mintAddress])
568
563
  return lodash.get(result, 'value.amount')
569
564
  }
@@ -590,7 +585,7 @@ export class Api {
590
585
  return tokensMint
591
586
  }
592
587
 
593
- async getTokenAccountsByOwner(address: string, tokenTicker: ?string): Promise<Array> {
588
+ async getTokenAccountsByOwner(address, tokenTicker) {
594
589
  const { value: accountsList } = await this.rpcCall(
595
590
  'getTokenAccountsByOwner',
596
591
  [address, { programId: TOKEN_PROGRAM_ID.toBase58() }, { encoding: 'jsonParsed' }],
@@ -637,7 +632,7 @@ export class Api {
637
632
  return tokensBalance
638
633
  }
639
634
 
640
- async isAssociatedTokenAccountActive(tokenAddress: string) {
635
+ async isAssociatedTokenAccountActive(tokenAddress) {
641
636
  // Returns the token balance of an SPL Token account.
642
637
  try {
643
638
  await this.rpcCall('getTokenAccountBalance', [tokenAddress])
@@ -648,40 +643,40 @@ export class Api {
648
643
  }
649
644
 
650
645
  // Returns account balance of a SPL Token account.
651
- async getTokenBalance(tokenAddress: string) {
646
+ async getTokenBalance(tokenAddress) {
652
647
  const result = await this.rpcCall('getTokenAccountBalance', [tokenAddress])
653
648
  return lodash.get(result, 'value.amount')
654
649
  }
655
650
 
656
- async getAccountInfo(address: string) {
651
+ async getAccountInfo(address, encoding = 'jsonParsed') {
657
652
  const { value } = await this.rpcCall(
658
653
  'getAccountInfo',
659
- [address, { encoding: 'jsonParsed', commitment: 'single' }],
654
+ [address, { encoding, commitment: 'single' }],
660
655
  { address }
661
656
  )
662
657
  return value
663
658
  }
664
659
 
665
- async isSpl(address: string) {
660
+ async isSpl(address) {
666
661
  const { owner } = await this.getAccountInfo(address)
667
662
  return owner === 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'
668
663
  }
669
664
 
670
- async getMetaplexMetadata(tokenMintAddress: string) {
665
+ async getMetaplexMetadata(tokenMintAddress) {
671
666
  const metaplexPDA = getMetadataAccount(tokenMintAddress)
672
- const res = await this.getAccountInfo(metaplexPDA)
667
+ const res = await this.getAccountInfo(metaplexPDA, 'base64')
673
668
  const data = lodash.get(res, 'data[0]')
674
669
  if (!data) return null
675
670
 
676
671
  return deserializeMetaplexMetadata(Buffer.from(data, 'base64'))
677
672
  }
678
673
 
679
- async getDecimals(tokenMintAddress: string) {
674
+ async getDecimals(tokenMintAddress) {
680
675
  const result = await this.rpcCall('getTokenSupply', [tokenMintAddress])
681
676
  return lodash.get(result, 'value.decimals', null)
682
677
  }
683
678
 
684
- async getAddressType(address: string) {
679
+ async getAddressType(address) {
685
680
  // solana, token or null (unknown), meaning address has never been initialized
686
681
  const value = await this.getAccountInfo(address)
687
682
  if (value === null) return null
@@ -699,7 +694,7 @@ export class Api {
699
694
  : null
700
695
  }
701
696
 
702
- async getTokenAddressOwner(address: string) {
697
+ async getTokenAddressOwner(address) {
703
698
  const value = await this.getAccountInfo(address)
704
699
  const owner = lodash.get(value, 'data.parsed.info.owner', null)
705
700
  return owner
@@ -711,17 +706,17 @@ export class Api {
711
706
  return mintAddress
712
707
  }
713
708
 
714
- async isTokenAddress(address: string) {
709
+ async isTokenAddress(address) {
715
710
  const type = await this.getAddressType(address)
716
711
  return type === 'token'
717
712
  }
718
713
 
719
- async isSOLaddress(address: string) {
714
+ async isSOLaddress(address) {
720
715
  const type = await this.getAddressType(address)
721
716
  return type === 'solana'
722
717
  }
723
718
 
724
- async getStakeAccountsInfo(address: string) {
719
+ async getStakeAccountsInfo(address) {
725
720
  const params = [
726
721
  STAKE_PROGRAM_ID.toBase58(),
727
722
  {
@@ -785,15 +780,15 @@ export class Api {
785
780
  return earnings
786
781
  }
787
782
 
788
- async getMinimumBalanceForRentExemption(size: number) {
783
+ async getMinimumBalanceForRentExemption(size) {
789
784
  return this.rpcCall('getMinimumBalanceForRentExemption', [size])
790
785
  }
791
786
 
792
- async getProgramAccounts(programId: string, config) {
787
+ async getProgramAccounts(programId, config) {
793
788
  return this.rpcCall('getProgramAccounts', [programId, config])
794
789
  }
795
790
 
796
- async getMultipleAccounts(pubkeys: string[], config) {
791
+ async getMultipleAccounts(pubkeys, config) {
797
792
  const response = await this.rpcCall('getMultipleAccounts', [pubkeys, config])
798
793
  return response && response.value ? response.value : []
799
794
  }
@@ -801,7 +796,7 @@ export class Api {
801
796
  /**
802
797
  * Broadcast a signed transaction
803
798
  */
804
- broadcastTransaction = async (signedTx: string, options): string => {
799
+ broadcastTransaction = async (signedTx, options) => {
805
800
  console.log('Solana broadcasting TX:', signedTx) // base64
806
801
  const defaultOptions = { encoding: 'base64', preflightCommitment: 'finalized' }
807
802
 
@@ -955,9 +950,9 @@ export class Api {
955
950
  * Simulate transaction and return side effects
956
951
  */
957
952
  simulateAndRetrieveSideEffects = async (
958
- message: SolanaWeb3Message,
959
- publicKey: string,
960
- transactionMessage?: any // decompiled TransactionMessage
953
+ message,
954
+ publicKey,
955
+ transactionMessage? // decompiled TransactionMessage
961
956
  ) => {
962
957
  const { config, accountAddresses } = getTransactionSimulationParams(
963
958
  transactionMessage || message
package/src/index.js CHANGED
@@ -8,6 +8,7 @@ export { default as SolanaFeeMonitor } from './fee-monitor'
8
8
  export * from './api'
9
9
  export * from './tx-log'
10
10
  export * from './account-state'
11
+ export * from './tx-send'
11
12
 
12
13
  // These are not the same asset objects as the wallet creates, so they should never be returned to the wallet.
13
14
  // Initially this may be violated by the Solana code until the first monitor tick updates assets with setTokens()
@@ -134,6 +134,7 @@ export class SolanaMonitor extends BaseMonitor {
134
134
  const assetName = _.get(tx, 'token.tokenName', baseAsset.name)
135
135
  const asset = this.assets[assetName]
136
136
  if (assetName === 'unknown' || !asset) continue // skip unknown tokens
137
+ const feeAsset = asset.feeAsset
137
138
 
138
139
  const coinAmount = asset.currency.baseUnit(tx.amount).toDefault()
139
140
 
@@ -150,6 +151,7 @@ export class SolanaMonitor extends BaseMonitor {
150
151
  unparsed: !!tx.unparsed,
151
152
  swapTx: !!(tx.data && tx.data.inner),
152
153
  },
154
+ currencies: { [assetName]: asset.currency, [feeAsset.name]: feeAsset.currency },
153
155
  }
154
156
 
155
157
  if (tx.owner === address) {
@@ -172,7 +174,6 @@ export class SolanaMonitor extends BaseMonitor {
172
174
  item.data.meta = tx.data.meta
173
175
  }
174
176
  if (asset.assetType === 'SOLANA_TOKEN' && item.feeAmount && item.feeAmount.isPositive) {
175
- const feeAsset = asset.feeAsset
176
177
  const feeItem = {
177
178
  ..._.clone(item),
178
179
  coinName: feeAsset.name,
package/src/tx-send.js ADDED
@@ -0,0 +1,202 @@
1
+ import { createUnsignedTx, findAssociatedTokenAddress } from '@exodus/solana-lib'
2
+ import assert from 'minimalistic-assert'
3
+
4
+ export const createAndBroadcastTXFactory = (api) => async (
5
+ { asset, walletAccount, address, amount, options = {} },
6
+ { assetClientInterface }
7
+ ) => {
8
+ const assetName = asset.name
9
+ assert(assetClientInterface, `assetClientInterface must be supplied in sendTx for ${assetName}`)
10
+
11
+ const {
12
+ feeAmount,
13
+ shouldLog = true,
14
+ method,
15
+ stakeAddresses,
16
+ seed,
17
+ pool,
18
+ customMintAddress,
19
+ tokenStandard,
20
+ // <MagicEden>
21
+ initializerAddress,
22
+ initializerDepositTokenAddress,
23
+ takerAmount,
24
+ escrowAddress,
25
+ escrowBump,
26
+ pdaAddress,
27
+ takerAddress,
28
+ expectedTakerAmount,
29
+ expectedMintAddress,
30
+ metadataAddress,
31
+ creators,
32
+ // </MagicEden>
33
+ reference,
34
+ memo,
35
+ } = options
36
+ let { recentBlockhash } = options
37
+ const { baseAsset } = asset
38
+ const from = await assetClientInterface.getReceiveAddress({
39
+ assetName: baseAsset.name,
40
+ walletAccount,
41
+ })
42
+ const currentAccountState = await assetClientInterface.getAccountState({
43
+ assetName: baseAsset.name,
44
+ walletAccount,
45
+ })
46
+
47
+ recentBlockhash = recentBlockhash || (await api.getRecentBlockHash())
48
+
49
+ let tokenParams = {}
50
+ if (asset.assetType === 'SOLANA_TOKEN' || customMintAddress) {
51
+ const tokenMintAddress = customMintAddress || asset.mintAddress
52
+ const tokenAddress = findAssociatedTokenAddress(address, tokenMintAddress)
53
+ const [
54
+ destinationAddressType,
55
+ isAssociatedTokenAccountActive,
56
+ fromTokenAccountAddresses,
57
+ ] = await Promise.all([
58
+ api.getAddressType(address),
59
+ api.isAssociatedTokenAccountActive(tokenAddress),
60
+ api.getTokenAccountsByOwner(from),
61
+ ])
62
+
63
+ const fromTokenAddresses = fromTokenAccountAddresses.filter(
64
+ ({ mintAddress }) => mintAddress === tokenMintAddress
65
+ )
66
+
67
+ tokenParams = {
68
+ tokenMintAddress,
69
+ destinationAddressType,
70
+ isAssociatedTokenAccountActive,
71
+ fromTokenAddresses,
72
+ tokenStandard,
73
+ }
74
+ }
75
+
76
+ const stakingParams = {
77
+ method,
78
+ stakeAddresses,
79
+ seed,
80
+ pool,
81
+ }
82
+
83
+ const magicEdenParams = {
84
+ method,
85
+ initializerAddress,
86
+ initializerDepositTokenAddress,
87
+ takerAmount,
88
+ escrowAddress,
89
+ escrowBump,
90
+ pdaAddress,
91
+ takerAddress,
92
+ expectedTakerAmount,
93
+ expectedMintAddress,
94
+ metadataAddress,
95
+ creators,
96
+ }
97
+
98
+ const unsignedTransaction = await createUnsignedTx({
99
+ asset,
100
+ from,
101
+ to: address,
102
+ amount,
103
+ fee: feeAmount,
104
+ recentBlockhash,
105
+ reference,
106
+ memo,
107
+ ...tokenParams,
108
+ ...stakingParams,
109
+ ...magicEdenParams,
110
+ })
111
+
112
+ const { txId, rawTx } = await assetClientInterface.signTransaction({
113
+ assetName: baseAsset.name,
114
+ unsignedTx: unsignedTransaction,
115
+ walletAccount,
116
+ })
117
+
118
+ await baseAsset.api.broadcastTx(rawTx)
119
+
120
+ const selfSend = from === address
121
+ const coinAmount = selfSend ? asset.currency.ZERO : amount.abs().negate()
122
+
123
+ if (shouldLog) {
124
+ const tx = {
125
+ txId,
126
+ confirmations: 0,
127
+ coinName: assetName,
128
+ coinAmount,
129
+ feeAmount,
130
+ feeCoinName: asset.feeAsset.name,
131
+ selfSend,
132
+ to: address,
133
+ data: {
134
+ // note
135
+ },
136
+ currencies: { [assetName]: asset.currency, [asset.feeAsset.name]: asset.feeAsset.currency },
137
+ }
138
+ await assetClientInterface.updateTxLogAndNotify({ assetName, walletAccount, txs: [tx] })
139
+ }
140
+
141
+ const changes = {}
142
+ if (asset.assetType === 'SOLANA_TOKEN') {
143
+ Object.assign(changes, {
144
+ balance: currentAccountState.balance.sub(feeAmount), // solana balance
145
+ tokenBalances: {
146
+ ...currentAccountState.tokenBalances,
147
+ [assetName]: currentAccountState.tokenBalances[assetName].sub(coinAmount.abs()),
148
+ }, // SPL token balance
149
+ })
150
+ // write tx entry in solana for token fee
151
+ const tx = {
152
+ txId,
153
+ coinName: baseAsset.name,
154
+ coinAmount: baseAsset.currency.ZERO,
155
+ tokens: [assetName],
156
+ feeAmount,
157
+ feeCoinName: baseAsset.feeAsset.name,
158
+ to: address,
159
+ selfSend,
160
+ currencies: {
161
+ [baseAsset.name]: baseAsset.currency,
162
+ [baseAsset.feeAsset.name]: baseAsset.feeAsset.currency,
163
+ },
164
+ }
165
+ await assetClientInterface.updateTxLogAndNotify({ assetName, walletAccount, txs: [tx] })
166
+ } else if (customMintAddress) {
167
+ Object.assign(changes, {
168
+ balance: currentAccountState.balance.sub(feeAmount), // solana balance
169
+ })
170
+ } else if (method) {
171
+ // staking: no changes
172
+ } else {
173
+ // SOL transfer
174
+ Object.assign(changes, {
175
+ balance: currentAccountState.balance.sub(coinAmount.abs()).sub(feeAmount),
176
+ })
177
+ }
178
+
179
+ if (method) {
180
+ const stakingData = { ...currentAccountState.mem }
181
+ switch (method) {
182
+ case 'delegate':
183
+ stakingData.isDelegating = true
184
+ stakingData.locked = stakingData.locked.add(amount)
185
+ break
186
+ case 'undelegate':
187
+ stakingData.isDelegating = false
188
+ stakingData.pending = stakingData.pending.add(stakingData.locked)
189
+ stakingData.locked = asset.currency.ZERO
190
+ break
191
+ case 'withdraw':
192
+ stakingData.withdrawable = asset.currency.ZERO
193
+ break
194
+ }
195
+
196
+ Object.assign(changes, { mem: stakingData })
197
+ }
198
+
199
+ await assetClientInterface.updateAccountState({ assetName, walletAccount, newData: changes })
200
+
201
+ return { txId }
202
+ }