@exodus/solana-api 2.5.31-alpha.0 → 2.5.31-alpha.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 +8 -3
- package/src/account-state.js +1 -0
- package/src/api.js +42 -39
- package/src/connection.js +15 -14
- package/src/get-balances.js +29 -30
- package/src/index.js +2 -1
- package/src/staking-utils.js +1 -3
- package/src/tx-log/solana-monitor.js +5 -3
- package/src/tx-send.js +162 -166
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/solana-api",
|
|
3
|
-
"version": "2.5.31-alpha.
|
|
3
|
+
"version": "2.5.31-alpha.2",
|
|
4
4
|
"description": "Exodus internal Solana asset API wrapper",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"files": [
|
|
@@ -14,6 +14,11 @@
|
|
|
14
14
|
"publishConfig": {
|
|
15
15
|
"access": "restricted"
|
|
16
16
|
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"test": "run -T jest",
|
|
19
|
+
"lint": "run -T eslint ./src",
|
|
20
|
+
"lint:fix": "yarn lint --fix"
|
|
21
|
+
},
|
|
17
22
|
"dependencies": {
|
|
18
23
|
"@exodus/asset-json-rpc": "^1.0.0",
|
|
19
24
|
"@exodus/asset-lib": "^4.0.0",
|
|
@@ -32,7 +37,7 @@
|
|
|
32
37
|
"wretch": "^1.5.2"
|
|
33
38
|
},
|
|
34
39
|
"devDependencies": {
|
|
35
|
-
"@exodus/assets-testing": "
|
|
40
|
+
"@exodus/assets-testing": "^1.0.0"
|
|
36
41
|
},
|
|
37
|
-
"gitHead": "
|
|
42
|
+
"gitHead": "9ec7084bcdfe14f1c6f11df393351885773046a6"
|
|
38
43
|
}
|
package/src/account-state.js
CHANGED
package/src/api.js
CHANGED
|
@@ -25,6 +25,7 @@ import { Connection } from './connection'
|
|
|
25
25
|
const RPC_URL = 'https://solana.a.exodus.io' // https://vip-api.mainnet-beta.solana.com/, https://api.mainnet-beta.solana.com
|
|
26
26
|
const WS_ENDPOINT = 'wss://solana.a.exodus.io/ws' // not standard across all node providers (we're compatible only with Quicknode)
|
|
27
27
|
const FORCE_HTTP = true // use https over ws
|
|
28
|
+
const TXS_LIMIT = 100
|
|
28
29
|
|
|
29
30
|
// Tokens + SOL api support
|
|
30
31
|
export class Api {
|
|
@@ -97,6 +98,7 @@ export class Api {
|
|
|
97
98
|
if (lodash.get(connection, 'isOpen') && !lodash.get(connection, 'shutdown') && !forceHttp) {
|
|
98
99
|
return connection.sendMessage(method, params)
|
|
99
100
|
}
|
|
101
|
+
|
|
100
102
|
// http fallback
|
|
101
103
|
return this.api.post({ method, params })
|
|
102
104
|
}
|
|
@@ -119,7 +121,7 @@ export class Api {
|
|
|
119
121
|
return state
|
|
120
122
|
}
|
|
121
123
|
|
|
122
|
-
async getRecentBlockHash(commitment
|
|
124
|
+
async getRecentBlockHash(commitment) {
|
|
123
125
|
const result = await this.rpcCall(
|
|
124
126
|
'getLatestBlockhash',
|
|
125
127
|
[{ commitment: commitment || 'finalized', encoding: 'jsonParsed' }],
|
|
@@ -168,7 +170,7 @@ export class Api {
|
|
|
168
170
|
// cursor is a txHash
|
|
169
171
|
|
|
170
172
|
try {
|
|
171
|
-
|
|
173
|
+
const until = cursor
|
|
172
174
|
|
|
173
175
|
const tokenAccountsByOwner = await this.getTokenAccountsByOwner(address) // Array
|
|
174
176
|
const tokenAccountAddresses = tokenAccountsByOwner
|
|
@@ -185,7 +187,7 @@ export class Api {
|
|
|
185
187
|
})
|
|
186
188
|
)
|
|
187
189
|
)
|
|
188
|
-
let txsId = txsResultsByAccount.
|
|
190
|
+
let txsId = txsResultsByAccount.flat().slice(0, TXS_LIMIT) // merge arrays
|
|
189
191
|
txsId = lodash.uniqBy(txsId, 'signature')
|
|
190
192
|
|
|
191
193
|
// get txs details in parallel
|
|
@@ -235,14 +237,8 @@ export class Api {
|
|
|
235
237
|
tokenAccountsByOwner,
|
|
236
238
|
{ includeUnparsed = false } = {}
|
|
237
239
|
) {
|
|
238
|
-
let {
|
|
239
|
-
|
|
240
|
-
preBalances,
|
|
241
|
-
postBalances,
|
|
242
|
-
preTokenBalances,
|
|
243
|
-
postTokenBalances,
|
|
244
|
-
innerInstructions,
|
|
245
|
-
} = txDetails.meta
|
|
240
|
+
let { fee, preBalances, postBalances, preTokenBalances, postTokenBalances, innerInstructions } =
|
|
241
|
+
txDetails.meta
|
|
246
242
|
preBalances = preBalances || []
|
|
247
243
|
postBalances = postBalances || []
|
|
248
244
|
preTokenBalances = preTokenBalances || []
|
|
@@ -381,7 +377,7 @@ export class Api {
|
|
|
381
377
|
const stakeTx = lodash.find(instructions, { program: 'system', type: 'createAccountWithSeed' })
|
|
382
378
|
const stakeWithdraw = lodash.find(instructions, { program: 'stake', type: 'withdraw' })
|
|
383
379
|
const stakeUndelegate = lodash.find(instructions, { program: 'stake', type: 'deactivate' })
|
|
384
|
-
const hasSolanaTx = solanaTx &&
|
|
380
|
+
const hasSolanaTx = solanaTx && preTokenBalances.length === 0 && postTokenBalances.length === 0 // only SOL moved and no tokens movements
|
|
385
381
|
|
|
386
382
|
let tx = {}
|
|
387
383
|
if (hasSolanaTx) {
|
|
@@ -448,7 +444,7 @@ export class Api {
|
|
|
448
444
|
Array.isArray(tokenAccountsByOwner),
|
|
449
445
|
'tokenAccountsByOwner is required when parsing token tx'
|
|
450
446
|
)
|
|
451
|
-
|
|
447
|
+
const tokenTxs = lodash
|
|
452
448
|
.filter(instructions, ({ program, type }) => {
|
|
453
449
|
return program === 'spl-token' && ['transfer', 'transferChecked'].includes(type)
|
|
454
450
|
}) // get Token transfer: could have more than 1 instructions
|
|
@@ -475,7 +471,7 @@ export class Api {
|
|
|
475
471
|
}
|
|
476
472
|
})
|
|
477
473
|
|
|
478
|
-
if (tokenTxs.length) {
|
|
474
|
+
if (tokenTxs.length > 0) {
|
|
479
475
|
// found spl-token simple transfer/transferChecked instruction
|
|
480
476
|
// .reduce to sum/sub (based on isSending) all the same tokens amount (From instructions -> 1 single tx)
|
|
481
477
|
tx = tokenTxs.reduce((finalTx, ix) => {
|
|
@@ -508,10 +504,10 @@ export class Api {
|
|
|
508
504
|
)
|
|
509
505
|
})
|
|
510
506
|
|
|
511
|
-
if (preBalances.length || postBalances.length) {
|
|
507
|
+
if (preBalances.length > 0 || postBalances.length > 0) {
|
|
512
508
|
tx = {}
|
|
513
509
|
|
|
514
|
-
if (includeUnparsed && innerInstructions.length) {
|
|
510
|
+
if (includeUnparsed && innerInstructions.length > 0) {
|
|
515
511
|
// when using includeUnparsed for DEX tx we want to keep SOL tx as "unparsed"
|
|
516
512
|
// 1. we want to treat all SOL dex transactions as "Contract transaction", not "Sent SOL"
|
|
517
513
|
// 2. default behavior is not perfect. For example it doesn't see SOL-side tx in
|
|
@@ -531,7 +527,7 @@ export class Api {
|
|
|
531
527
|
}
|
|
532
528
|
|
|
533
529
|
// If it has inner instructions then it's a DEX tx that moved SPL -> SPL
|
|
534
|
-
if (innerInstructions.length) {
|
|
530
|
+
if (innerInstructions.length > 0) {
|
|
535
531
|
tx.dexTxs = innerInstructions
|
|
536
532
|
// if tx involves only SPL swaps. Expand DEX ix (first element as tx base and the other kept there)
|
|
537
533
|
if (!tx.from && !solanaTx) {
|
|
@@ -571,7 +567,7 @@ export class Api {
|
|
|
571
567
|
|
|
572
568
|
async getWalletTokensList({ tokenAccounts }) {
|
|
573
569
|
const tokensMint = []
|
|
574
|
-
for (
|
|
570
|
+
for (const account of tokenAccounts) {
|
|
575
571
|
const mint = account.mintAddress
|
|
576
572
|
|
|
577
573
|
// skip cached NFT
|
|
@@ -584,6 +580,7 @@ export class Api {
|
|
|
584
580
|
this.tokensToSkip[mint] = true
|
|
585
581
|
continue
|
|
586
582
|
}
|
|
583
|
+
|
|
587
584
|
// OK
|
|
588
585
|
tokensMint.push(mint)
|
|
589
586
|
}
|
|
@@ -599,7 +596,7 @@ export class Api {
|
|
|
599
596
|
)
|
|
600
597
|
|
|
601
598
|
const tokenAccounts = []
|
|
602
|
-
for (
|
|
599
|
+
for (const entry of accountsList) {
|
|
603
600
|
const { pubkey, account } = entry
|
|
604
601
|
|
|
605
602
|
const mint = lodash.get(account, 'data.parsed.info.mint')
|
|
@@ -617,6 +614,7 @@ export class Api {
|
|
|
617
614
|
mintAddress: mint,
|
|
618
615
|
})
|
|
619
616
|
}
|
|
617
|
+
|
|
620
618
|
// eventually filter by token
|
|
621
619
|
return tokenTicker
|
|
622
620
|
? tokenAccounts.filter(({ ticker }) => ticker === tokenTicker)
|
|
@@ -624,18 +622,24 @@ export class Api {
|
|
|
624
622
|
}
|
|
625
623
|
|
|
626
624
|
async getTokensBalance({ address, filterByTokens = [], tokenAccounts }) {
|
|
627
|
-
|
|
625
|
+
const accounts = tokenAccounts || (await this.getTokenAccountsByOwner(address))
|
|
628
626
|
|
|
629
|
-
|
|
630
|
-
if (
|
|
627
|
+
return accounts.reduce((acc, { tokenName, balance }) => {
|
|
628
|
+
if (
|
|
629
|
+
tokenName === 'unknown' ||
|
|
630
|
+
(filterByTokens.length > 0 && !filterByTokens.includes(tokenName))
|
|
631
|
+
)
|
|
631
632
|
return acc // filter by supported tokens only
|
|
632
|
-
if (
|
|
633
|
+
if (acc[tokenName]) {
|
|
634
|
+
acc[tokenName] += Number(balance)
|
|
635
|
+
}
|
|
633
636
|
// e.g { 'serum': 123 }
|
|
634
|
-
else
|
|
637
|
+
else {
|
|
638
|
+
acc[tokenName] = Number(balance)
|
|
639
|
+
} // merge same token account balance
|
|
640
|
+
|
|
635
641
|
return acc
|
|
636
642
|
}, {})
|
|
637
|
-
|
|
638
|
-
return tokensBalance
|
|
639
643
|
}
|
|
640
644
|
|
|
641
645
|
async isAssociatedTokenAccountActive(tokenAddress) {
|
|
@@ -643,7 +647,7 @@ export class Api {
|
|
|
643
647
|
try {
|
|
644
648
|
await this.rpcCall('getTokenAccountBalance', [tokenAddress])
|
|
645
649
|
return true
|
|
646
|
-
} catch
|
|
650
|
+
} catch {
|
|
647
651
|
return false
|
|
648
652
|
}
|
|
649
653
|
}
|
|
@@ -696,20 +700,19 @@ export class Api {
|
|
|
696
700
|
return account.owner === SYSTEM_PROGRAM_ID.toBase58()
|
|
697
701
|
? 'solana'
|
|
698
702
|
: account.owner === TOKEN_PROGRAM_ID.toBase58()
|
|
699
|
-
|
|
700
|
-
|
|
703
|
+
? 'token'
|
|
704
|
+
: null
|
|
701
705
|
}
|
|
702
706
|
|
|
703
707
|
async getTokenAddressOwner(address) {
|
|
704
708
|
const value = await this.getAccountInfo(address)
|
|
705
|
-
|
|
706
|
-
return owner
|
|
709
|
+
return lodash.get(value, 'data.parsed.info.owner', null)
|
|
707
710
|
}
|
|
708
711
|
|
|
709
712
|
async getAddressMint(address) {
|
|
710
713
|
const value = await this.getAccountInfo(address)
|
|
711
|
-
|
|
712
|
-
return
|
|
714
|
+
// token mint
|
|
715
|
+
return lodash.get(value, 'data.parsed.info.mint', null)
|
|
713
716
|
}
|
|
714
717
|
|
|
715
718
|
async isTokenAddress(address) {
|
|
@@ -744,7 +747,7 @@ export class Api {
|
|
|
744
747
|
let locked = 0
|
|
745
748
|
let withdrawable = 0
|
|
746
749
|
let pending = 0
|
|
747
|
-
for (
|
|
750
|
+
for (const entry of res) {
|
|
748
751
|
const addr = entry.pubkey
|
|
749
752
|
const lamports = lodash.get(entry, 'account.lamports', 0)
|
|
750
753
|
const delegation = lodash.get(entry, 'account.data.parsed.info.stake.delegation', {})
|
|
@@ -765,11 +768,12 @@ export class Api {
|
|
|
765
768
|
withdrawable += accounts[addr].canWithdraw ? lamports : 0
|
|
766
769
|
pending += accounts[addr].isDeactivating ? lamports : 0
|
|
767
770
|
}
|
|
771
|
+
|
|
768
772
|
return { accounts, totalStake, locked, withdrawable, pending }
|
|
769
773
|
}
|
|
770
774
|
|
|
771
775
|
async getRewards(stakingAddresses = []) {
|
|
772
|
-
if (
|
|
776
|
+
if (stakingAddresses.length === 0) return 0
|
|
773
777
|
|
|
774
778
|
// custom endpoint!
|
|
775
779
|
const rewards = await this.request('rewards')
|
|
@@ -779,11 +783,9 @@ export class Api {
|
|
|
779
783
|
.json()
|
|
780
784
|
|
|
781
785
|
// sum rewards for all addresses
|
|
782
|
-
|
|
786
|
+
return Object.values(rewards).reduce((total, x) => {
|
|
783
787
|
return total + x
|
|
784
788
|
}, 0)
|
|
785
|
-
|
|
786
|
-
return earnings
|
|
787
789
|
}
|
|
788
790
|
|
|
789
791
|
async getMinimumBalanceForRentExemption(size) {
|
|
@@ -867,6 +869,7 @@ export class Api {
|
|
|
867
869
|
if (error.message && error.message.includes('could not find account')) {
|
|
868
870
|
return defaultValue
|
|
869
871
|
}
|
|
872
|
+
|
|
870
873
|
throw error
|
|
871
874
|
}
|
|
872
875
|
}
|
|
@@ -958,7 +961,7 @@ export class Api {
|
|
|
958
961
|
simulateAndRetrieveSideEffects = async (
|
|
959
962
|
message,
|
|
960
963
|
publicKey,
|
|
961
|
-
transactionMessage
|
|
964
|
+
transactionMessage // decompiled TransactionMessage
|
|
962
965
|
) => {
|
|
963
966
|
const { config, accountAddresses } = getTransactionSimulationParams(
|
|
964
967
|
transactionMessage || message
|
package/src/connection.js
CHANGED
|
@@ -67,8 +67,8 @@ export class Connection {
|
|
|
67
67
|
debug('Opening WS to:', reqUrl)
|
|
68
68
|
const ws = new WebSocket(`${reqUrl}`)
|
|
69
69
|
ws.onmessage = this.onMessage.bind(this)
|
|
70
|
-
ws.
|
|
71
|
-
ws.
|
|
70
|
+
ws.addEventListener('open', this.onOpen.bind(this))
|
|
71
|
+
ws.addEventListener('close', this.onClose.bind(this))
|
|
72
72
|
ws.onerror = this.onError.bind(this)
|
|
73
73
|
return ws
|
|
74
74
|
}
|
|
@@ -90,14 +90,14 @@ export class Connection {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
get running() {
|
|
93
|
-
return !!(!this.isClosed || this.inProcessMessages || this.messageQueue.length)
|
|
93
|
+
return !!(!this.isClosed || this.inProcessMessages || this.messageQueue.length > 0)
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
get connectionState() {
|
|
97
97
|
if (this.isConnecting) return 'CONNECTING'
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
98
|
+
if (this.isOpen) return 'OPEN'
|
|
99
|
+
if (this.isClosing) return 'CLOSING'
|
|
100
|
+
if (this.isClosed) return 'CLOSED'
|
|
101
101
|
return 'NONE'
|
|
102
102
|
}
|
|
103
103
|
|
|
@@ -125,7 +125,13 @@ export class Connection {
|
|
|
125
125
|
try {
|
|
126
126
|
const json = JSON.parse(evt.data)
|
|
127
127
|
debug('new ws msg:', json)
|
|
128
|
-
if (
|
|
128
|
+
if (json.error) {
|
|
129
|
+
if (lodash.get(this.rpcQueue, json.id)) {
|
|
130
|
+
this.rpcQueue[json.id].reject(new Error(json.error.message))
|
|
131
|
+
clearTimeout(this.rpcQueue[json.id].timeout)
|
|
132
|
+
delete this.rpcQueue[json.id]
|
|
133
|
+
} else debug('Unsupported WS message:', json.error.message)
|
|
134
|
+
} else {
|
|
129
135
|
if (lodash.get(this.rpcQueue, json.id)) {
|
|
130
136
|
// json-rpc reply
|
|
131
137
|
clearTimeout(this.rpcQueue[json.id].timeout)
|
|
@@ -136,13 +142,8 @@ export class Connection {
|
|
|
136
142
|
debug('pushing msg to queue', msg)
|
|
137
143
|
this.messageQueue.push(msg) // sub results
|
|
138
144
|
}
|
|
145
|
+
|
|
139
146
|
this.processMessages()
|
|
140
|
-
} else {
|
|
141
|
-
if (lodash.get(this.rpcQueue, json.id)) {
|
|
142
|
-
this.rpcQueue[json.id].reject(new Error(json.error.message))
|
|
143
|
-
clearTimeout(this.rpcQueue[json.id].timeout)
|
|
144
|
-
delete this.rpcQueue[json.id]
|
|
145
|
-
} else debug('Unsupported WS message:', json.error.message)
|
|
146
147
|
}
|
|
147
148
|
} catch (e) {
|
|
148
149
|
debug(e)
|
|
@@ -199,7 +200,7 @@ export class Connection {
|
|
|
199
200
|
if (this.inProcessMessages) return null
|
|
200
201
|
this.inProcessMessages = true
|
|
201
202
|
try {
|
|
202
|
-
while (this.messageQueue.length) {
|
|
203
|
+
while (this.messageQueue.length > 0) {
|
|
203
204
|
const items = this.messageQueue.splice(0, this.messageQueue.length)
|
|
204
205
|
await this.callback(items)
|
|
205
206
|
}
|
package/src/get-balances.js
CHANGED
|
@@ -3,35 +3,33 @@ import { TxSet } from '@exodus/models'
|
|
|
3
3
|
// staking may be a feature that may not be available for a given wallet.
|
|
4
4
|
// In this case, The wallet should exclude the staking balance from the general balance
|
|
5
5
|
|
|
6
|
-
export const getBalancesFactory =
|
|
7
|
-
|
|
8
|
-
accountState,
|
|
9
|
-
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
asset
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return { balance, spendableBalance: balance }
|
|
22
|
-
}
|
|
6
|
+
export const getBalancesFactory =
|
|
7
|
+
({ stakingFeatureAvailable }) =>
|
|
8
|
+
({ asset, accountState, txLog }) => {
|
|
9
|
+
const zero = asset.currency.ZERO
|
|
10
|
+
const { balance, locked, withdrawable, pending } = fixBalances({
|
|
11
|
+
txLog,
|
|
12
|
+
balance: getBalanceFromAccountState({ asset, accountState }),
|
|
13
|
+
locked: accountState.mem?.locked || zero,
|
|
14
|
+
withdrawable: accountState.mem?.withdrawable || zero,
|
|
15
|
+
pending: accountState.mem?.pending || zero,
|
|
16
|
+
asset,
|
|
17
|
+
})
|
|
18
|
+
if (asset.baseAsset.name !== asset.name) {
|
|
19
|
+
return { balance, spendableBalance: balance }
|
|
20
|
+
}
|
|
23
21
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
22
|
+
const balanceWithoutStaking = balance
|
|
23
|
+
.sub(locked)
|
|
24
|
+
.sub(withdrawable)
|
|
25
|
+
.sub(pending)
|
|
26
|
+
.clampLowerZero()
|
|
29
27
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
return {
|
|
29
|
+
balance: stakingFeatureAvailable ? balance : balanceWithoutStaking,
|
|
30
|
+
spendableBalance: balanceWithoutStaking.sub(asset.accountReserve || zero).clampLowerZero(),
|
|
31
|
+
}
|
|
33
32
|
}
|
|
34
|
-
}
|
|
35
33
|
|
|
36
34
|
const fixBalances = ({ txLog = TxSet.EMPTY, balance, locked, withdrawable, pending, asset }) => {
|
|
37
35
|
for (const tx of txLog) {
|
|
@@ -40,10 +38,7 @@ const fixBalances = ({ txLog = TxSet.EMPTY, balance, locked, withdrawable, pendi
|
|
|
40
38
|
balance = balance.sub(tx.feeAmount)
|
|
41
39
|
}
|
|
42
40
|
|
|
43
|
-
if (
|
|
44
|
-
// coinAmount is negative for sent tx
|
|
45
|
-
balance = balance.sub(tx.coinAmount.abs())
|
|
46
|
-
} else {
|
|
41
|
+
if (tx.data.staking) {
|
|
47
42
|
// staking tx
|
|
48
43
|
switch (tx.data.staking?.method) {
|
|
49
44
|
case 'delegate':
|
|
@@ -57,9 +52,13 @@ const fixBalances = ({ txLog = TxSet.EMPTY, balance, locked, withdrawable, pendi
|
|
|
57
52
|
locked = asset.currency.ZERO
|
|
58
53
|
break
|
|
59
54
|
}
|
|
55
|
+
} else {
|
|
56
|
+
// coinAmount is negative for sent tx
|
|
57
|
+
balance = balance.sub(tx.coinAmount.abs())
|
|
60
58
|
}
|
|
61
59
|
}
|
|
62
60
|
}
|
|
61
|
+
|
|
63
62
|
return {
|
|
64
63
|
balance: balance.clampLowerZero(),
|
|
65
64
|
locked: locked.clampLowerZero(),
|
package/src/index.js
CHANGED
|
@@ -4,7 +4,6 @@ import assetsList from '@exodus/solana-meta'
|
|
|
4
4
|
|
|
5
5
|
import { Api } from './api'
|
|
6
6
|
|
|
7
|
-
export { Api }
|
|
8
7
|
export { default as SolanaFeeMonitor } from './fee-monitor'
|
|
9
8
|
export { SolanaMonitor } from './tx-log'
|
|
10
9
|
export { SolanaAccountState } from './account-state'
|
|
@@ -26,3 +25,5 @@ const assets = connectAssets(keyBy(assetsList, (asset) => asset.name))
|
|
|
26
25
|
// Clients should not call an specific server api directly.
|
|
27
26
|
const serverApi = new Api({ assets })
|
|
28
27
|
export default serverApi
|
|
28
|
+
|
|
29
|
+
export { Api } from './api'
|
package/src/staking-utils.js
CHANGED
|
@@ -9,9 +9,7 @@ export const getSolStakedFee = ({ asset, stakingInfo, fee }) => {
|
|
|
9
9
|
const { accounts } = stakingInfo
|
|
10
10
|
|
|
11
11
|
const allPending = Object.entries(accounts).length
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
return pendingFee
|
|
12
|
+
return allPending > 0 ? fee.mul(allPending) : currency.ZERO
|
|
15
13
|
}
|
|
16
14
|
|
|
17
15
|
export const getStakingInfo = (accountMem) => {
|
|
@@ -97,10 +97,10 @@ export class SolanaMonitor extends BaseMonitor {
|
|
|
97
97
|
|
|
98
98
|
async getStakingAddressesFromTxLog({ assetName, walletAccount }) {
|
|
99
99
|
const txLog = await this.aci.getTxLog({ assetName: this.asset.name, walletAccount })
|
|
100
|
-
const stakingAddresses =
|
|
100
|
+
const stakingAddresses = [...txLog]
|
|
101
101
|
.filter((tx) => _.get(tx, 'data.staking.stakeAddresses'))
|
|
102
102
|
.map((tx) => tx.data.staking.stakeAddresses)
|
|
103
|
-
return _.uniq(
|
|
103
|
+
return _.uniq(stakingAddresses.flat())
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
balanceChanged({ account, newAccount }) {
|
|
@@ -173,7 +173,7 @@ export class SolanaMonitor extends BaseMonitor {
|
|
|
173
173
|
}
|
|
174
174
|
|
|
175
175
|
async getHistory({ address, accountState, refresh } = {}) {
|
|
176
|
-
|
|
176
|
+
const cursor = refresh ? '' : accountState.cursor
|
|
177
177
|
const baseAsset = this.asset
|
|
178
178
|
|
|
179
179
|
const { transactions, newCursor } = await this.api.getTransactions(address, {
|
|
@@ -226,6 +226,7 @@ export class SolanaMonitor extends BaseMonitor {
|
|
|
226
226
|
|
|
227
227
|
item.data.meta = tx.data.meta
|
|
228
228
|
}
|
|
229
|
+
|
|
229
230
|
if (asset.assetType === 'SOLANA_TOKEN' && item.feeAmount && item.feeAmount.isPositive) {
|
|
230
231
|
const feeItem = {
|
|
231
232
|
..._.clone(item),
|
|
@@ -235,6 +236,7 @@ export class SolanaMonitor extends BaseMonitor {
|
|
|
235
236
|
}
|
|
236
237
|
mappedTransactions.push(feeItem)
|
|
237
238
|
}
|
|
239
|
+
|
|
238
240
|
mappedTransactions.push(item)
|
|
239
241
|
}
|
|
240
242
|
|
package/src/tx-send.js
CHANGED
|
@@ -1,183 +1,179 @@
|
|
|
1
1
|
import { createUnsignedTx, findAssociatedTokenAddress } from '@exodus/solana-lib'
|
|
2
2
|
import assert from 'minimalistic-assert'
|
|
3
3
|
|
|
4
|
-
export const createAndBroadcastTXFactory =
|
|
5
|
-
|
|
6
|
-
{ assetClientInterface }
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
4
|
+
export const createAndBroadcastTXFactory =
|
|
5
|
+
(api) =>
|
|
6
|
+
async ({ asset, walletAccount, address, amount, options = {} }, { assetClientInterface }) => {
|
|
7
|
+
const assetName = asset.name
|
|
8
|
+
assert(assetClientInterface, `assetClientInterface must be supplied in sendTx for ${assetName}`)
|
|
9
|
+
|
|
10
|
+
const {
|
|
11
|
+
feeAmount,
|
|
12
|
+
method,
|
|
13
|
+
stakeAddresses,
|
|
14
|
+
seed,
|
|
15
|
+
pool,
|
|
16
|
+
customMintAddress,
|
|
17
|
+
tokenStandard,
|
|
18
|
+
// <MagicEden>
|
|
19
|
+
initializerAddress,
|
|
20
|
+
initializerDepositTokenAddress,
|
|
21
|
+
takerAmount,
|
|
22
|
+
escrowAddress,
|
|
23
|
+
escrowBump,
|
|
24
|
+
pdaAddress,
|
|
25
|
+
takerAddress,
|
|
26
|
+
expectedTakerAmount,
|
|
27
|
+
expectedMintAddress,
|
|
28
|
+
metadataAddress,
|
|
29
|
+
creators,
|
|
30
|
+
// </MagicEden>
|
|
31
|
+
reference,
|
|
32
|
+
memo,
|
|
33
|
+
} = options
|
|
34
|
+
const { baseAsset } = asset
|
|
35
|
+
const from = await assetClientInterface.getReceiveAddress({
|
|
36
|
+
assetName: baseAsset.name,
|
|
37
|
+
walletAccount,
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const isToken = asset.assetType === 'SOLANA_TOKEN'
|
|
41
|
+
|
|
42
|
+
// Check if receiver has address active when sending tokens.
|
|
43
|
+
if (isToken) {
|
|
44
|
+
// check address mint is the same
|
|
45
|
+
const targetMint = await api.getAddressMint(address) // null if it's a SOL address
|
|
46
|
+
if (targetMint && targetMint !== asset.mintAddress) {
|
|
47
|
+
const err = new Error('Wrong Destination Wallet')
|
|
48
|
+
err.reason = { mintAddressMismatch: true }
|
|
49
|
+
throw err
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
// sending SOL
|
|
53
|
+
const addressType = await api.getAddressType(address)
|
|
54
|
+
if (addressType === 'token') {
|
|
55
|
+
const err = new Error('Destination Wallet is a Token address')
|
|
56
|
+
err.reason = { wrongAddressType: true }
|
|
57
|
+
throw err
|
|
58
|
+
}
|
|
51
59
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
60
|
+
|
|
61
|
+
const recentBlockhash = options.recentBlockhash || (await api.getRecentBlockHash())
|
|
62
|
+
|
|
63
|
+
let tokenParams = Object.create(null)
|
|
64
|
+
if (isToken || customMintAddress) {
|
|
65
|
+
const tokenMintAddress = customMintAddress || asset.mintAddress
|
|
66
|
+
const tokenAddress = findAssociatedTokenAddress(address, tokenMintAddress)
|
|
67
|
+
const [destinationAddressType, isAssociatedTokenAccountActive, fromTokenAccountAddresses] =
|
|
68
|
+
await Promise.all([
|
|
69
|
+
api.getAddressType(address),
|
|
70
|
+
api.isAssociatedTokenAccountActive(tokenAddress),
|
|
71
|
+
api.getTokenAccountsByOwner(from),
|
|
72
|
+
])
|
|
73
|
+
|
|
74
|
+
const fromTokenAddresses = fromTokenAccountAddresses.filter(
|
|
75
|
+
({ mintAddress }) => mintAddress === tokenMintAddress
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
tokenParams = {
|
|
79
|
+
tokenMintAddress,
|
|
80
|
+
destinationAddressType,
|
|
81
|
+
isAssociatedTokenAccountActive,
|
|
82
|
+
fromTokenAddresses,
|
|
83
|
+
tokenStandard,
|
|
84
|
+
}
|
|
59
85
|
}
|
|
60
|
-
}
|
|
61
86
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const tokenAddress = findAssociatedTokenAddress(address, tokenMintAddress)
|
|
68
|
-
const [
|
|
69
|
-
destinationAddressType,
|
|
70
|
-
isAssociatedTokenAccountActive,
|
|
71
|
-
fromTokenAccountAddresses,
|
|
72
|
-
] = await Promise.all([
|
|
73
|
-
api.getAddressType(address),
|
|
74
|
-
api.isAssociatedTokenAccountActive(tokenAddress),
|
|
75
|
-
api.getTokenAccountsByOwner(from),
|
|
76
|
-
])
|
|
77
|
-
|
|
78
|
-
const fromTokenAddresses = fromTokenAccountAddresses.filter(
|
|
79
|
-
({ mintAddress }) => mintAddress === tokenMintAddress
|
|
80
|
-
)
|
|
81
|
-
|
|
82
|
-
tokenParams = {
|
|
83
|
-
tokenMintAddress,
|
|
84
|
-
destinationAddressType,
|
|
85
|
-
isAssociatedTokenAccountActive,
|
|
86
|
-
fromTokenAddresses,
|
|
87
|
-
tokenStandard,
|
|
87
|
+
const stakingParams = {
|
|
88
|
+
method,
|
|
89
|
+
stakeAddresses,
|
|
90
|
+
seed,
|
|
91
|
+
pool,
|
|
88
92
|
}
|
|
89
|
-
}
|
|
90
93
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
const magicEdenParams = {
|
|
95
|
+
method,
|
|
96
|
+
initializerAddress,
|
|
97
|
+
initializerDepositTokenAddress,
|
|
98
|
+
takerAmount,
|
|
99
|
+
escrowAddress,
|
|
100
|
+
escrowBump,
|
|
101
|
+
pdaAddress,
|
|
102
|
+
takerAddress,
|
|
103
|
+
expectedTakerAmount,
|
|
104
|
+
expectedMintAddress,
|
|
105
|
+
metadataAddress,
|
|
106
|
+
creators,
|
|
107
|
+
}
|
|
97
108
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
109
|
+
const unsignedTransaction = createUnsignedTx({
|
|
110
|
+
asset,
|
|
111
|
+
from,
|
|
112
|
+
to: address,
|
|
113
|
+
amount,
|
|
114
|
+
fee: feeAmount,
|
|
115
|
+
recentBlockhash,
|
|
116
|
+
reference,
|
|
117
|
+
memo,
|
|
118
|
+
...tokenParams,
|
|
119
|
+
...stakingParams,
|
|
120
|
+
...magicEdenParams,
|
|
121
|
+
})
|
|
112
122
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
reference,
|
|
121
|
-
memo,
|
|
122
|
-
...tokenParams,
|
|
123
|
-
...stakingParams,
|
|
124
|
-
...magicEdenParams,
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
const { txId, rawTx } = await assetClientInterface.signTransaction({
|
|
128
|
-
assetName: baseAsset.name,
|
|
129
|
-
unsignedTx: unsignedTransaction,
|
|
130
|
-
walletAccount,
|
|
131
|
-
})
|
|
132
|
-
|
|
133
|
-
await baseAsset.api.broadcastTx(rawTx)
|
|
134
|
-
|
|
135
|
-
const selfSend = from === address
|
|
136
|
-
const isStakingTx = ['delegate', 'undelegate', 'withdraw'].includes(method)
|
|
137
|
-
const coinAmount = isStakingTx
|
|
138
|
-
? amount.abs()
|
|
139
|
-
: selfSend
|
|
140
|
-
? asset.currency.ZERO
|
|
141
|
-
: amount.abs().negate()
|
|
142
|
-
|
|
143
|
-
const data = isStakingTx ? { staking: stakingParams } : Object.create(null)
|
|
144
|
-
const tx = {
|
|
145
|
-
txId,
|
|
146
|
-
confirmations: 0,
|
|
147
|
-
coinName: assetName,
|
|
148
|
-
coinAmount,
|
|
149
|
-
feeAmount,
|
|
150
|
-
feeCoinName: asset.feeAsset.name,
|
|
151
|
-
selfSend,
|
|
152
|
-
to: address,
|
|
153
|
-
data,
|
|
154
|
-
currencies: { [assetName]: asset.currency, [asset.feeAsset.name]: asset.feeAsset.currency },
|
|
155
|
-
}
|
|
156
|
-
await assetClientInterface.updateTxLogAndNotify({ assetName, walletAccount, txs: [tx] })
|
|
123
|
+
const { txId, rawTx } = await assetClientInterface.signTransaction({
|
|
124
|
+
assetName: baseAsset.name,
|
|
125
|
+
unsignedTx: unsignedTransaction,
|
|
126
|
+
walletAccount,
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
await baseAsset.api.broadcastTx(rawTx)
|
|
157
130
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const
|
|
131
|
+
const selfSend = from === address
|
|
132
|
+
const isStakingTx = ['delegate', 'undelegate', 'withdraw'].includes(method)
|
|
133
|
+
const coinAmount = isStakingTx
|
|
134
|
+
? amount.abs()
|
|
135
|
+
: selfSend
|
|
136
|
+
? asset.currency.ZERO
|
|
137
|
+
: amount.abs().negate()
|
|
138
|
+
|
|
139
|
+
const data = isStakingTx ? { staking: stakingParams } : Object.create(null)
|
|
140
|
+
const tx = {
|
|
161
141
|
txId,
|
|
162
142
|
confirmations: 0,
|
|
163
|
-
coinName:
|
|
164
|
-
coinAmount
|
|
165
|
-
tokens: [assetName],
|
|
143
|
+
coinName: assetName,
|
|
144
|
+
coinAmount,
|
|
166
145
|
feeAmount,
|
|
167
|
-
feeCoinName:
|
|
168
|
-
to: address,
|
|
146
|
+
feeCoinName: asset.feeAsset.name,
|
|
169
147
|
selfSend,
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
148
|
+
to: address,
|
|
149
|
+
data,
|
|
150
|
+
currencies: { [assetName]: asset.currency, [asset.feeAsset.name]: asset.feeAsset.currency },
|
|
151
|
+
}
|
|
152
|
+
await assetClientInterface.updateTxLogAndNotify({ assetName, walletAccount, txs: [tx] })
|
|
153
|
+
|
|
154
|
+
if (isToken) {
|
|
155
|
+
// write tx entry in solana for token fee
|
|
156
|
+
const txForFee = {
|
|
157
|
+
txId,
|
|
158
|
+
confirmations: 0,
|
|
159
|
+
coinName: baseAsset.name,
|
|
160
|
+
coinAmount: baseAsset.currency.ZERO,
|
|
161
|
+
tokens: [assetName],
|
|
162
|
+
feeAmount,
|
|
163
|
+
feeCoinName: baseAsset.feeAsset.name,
|
|
164
|
+
to: address,
|
|
165
|
+
selfSend,
|
|
166
|
+
currencies: {
|
|
167
|
+
[baseAsset.name]: baseAsset.currency,
|
|
168
|
+
[baseAsset.feeAsset.name]: baseAsset.feeAsset.currency,
|
|
169
|
+
},
|
|
170
|
+
}
|
|
171
|
+
await assetClientInterface.updateTxLogAndNotify({
|
|
172
|
+
assetName: baseAsset.name,
|
|
173
|
+
walletAccount,
|
|
174
|
+
txs: [txForFee],
|
|
175
|
+
})
|
|
174
176
|
}
|
|
175
|
-
await assetClientInterface.updateTxLogAndNotify({
|
|
176
|
-
assetName: baseAsset.name,
|
|
177
|
-
walletAccount,
|
|
178
|
-
txs: [txForFee],
|
|
179
|
-
})
|
|
180
|
-
}
|
|
181
177
|
|
|
182
|
-
|
|
183
|
-
}
|
|
178
|
+
return { txId }
|
|
179
|
+
}
|