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