@exodus/solana-api 2.5.31-alpha.1 → 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 +7 -2
- package/src/account-state.js +1 -0
- package/src/api.js +43 -42
- package/src/connection.js +17 -19
- 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 +68 -21
- 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
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 {
|
|
@@ -59,18 +60,16 @@ export class Api {
|
|
|
59
60
|
async watchAddress({
|
|
60
61
|
address,
|
|
61
62
|
tokensAddresses = [],
|
|
62
|
-
onMessage,
|
|
63
63
|
handleAccounts,
|
|
64
64
|
handleTransfers,
|
|
65
65
|
handleReconnect,
|
|
66
66
|
reconnectDelay,
|
|
67
67
|
}) {
|
|
68
|
-
if (
|
|
68
|
+
if (FORCE_HTTP) return false
|
|
69
69
|
const conn = new Connection({
|
|
70
70
|
endpoint: this.wsUrl,
|
|
71
71
|
address,
|
|
72
72
|
tokensAddresses,
|
|
73
|
-
onMsg: (json) => onMessage(json),
|
|
74
73
|
callback: (updates) =>
|
|
75
74
|
this.handleUpdates({ updates, address, handleAccounts, handleTransfers }),
|
|
76
75
|
reconnectCallback: handleReconnect,
|
|
@@ -99,6 +98,7 @@ export class Api {
|
|
|
99
98
|
if (lodash.get(connection, 'isOpen') && !lodash.get(connection, 'shutdown') && !forceHttp) {
|
|
100
99
|
return connection.sendMessage(method, params)
|
|
101
100
|
}
|
|
101
|
+
|
|
102
102
|
// http fallback
|
|
103
103
|
return this.api.post({ method, params })
|
|
104
104
|
}
|
|
@@ -121,7 +121,7 @@ export class Api {
|
|
|
121
121
|
return state
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
async getRecentBlockHash(commitment
|
|
124
|
+
async getRecentBlockHash(commitment) {
|
|
125
125
|
const result = await this.rpcCall(
|
|
126
126
|
'getLatestBlockhash',
|
|
127
127
|
[{ commitment: commitment || 'finalized', encoding: 'jsonParsed' }],
|
|
@@ -170,7 +170,7 @@ export class Api {
|
|
|
170
170
|
// cursor is a txHash
|
|
171
171
|
|
|
172
172
|
try {
|
|
173
|
-
|
|
173
|
+
const until = cursor
|
|
174
174
|
|
|
175
175
|
const tokenAccountsByOwner = await this.getTokenAccountsByOwner(address) // Array
|
|
176
176
|
const tokenAccountAddresses = tokenAccountsByOwner
|
|
@@ -187,7 +187,7 @@ export class Api {
|
|
|
187
187
|
})
|
|
188
188
|
)
|
|
189
189
|
)
|
|
190
|
-
let txsId = txsResultsByAccount.
|
|
190
|
+
let txsId = txsResultsByAccount.flat().slice(0, TXS_LIMIT) // merge arrays
|
|
191
191
|
txsId = lodash.uniqBy(txsId, 'signature')
|
|
192
192
|
|
|
193
193
|
// get txs details in parallel
|
|
@@ -237,14 +237,8 @@ export class Api {
|
|
|
237
237
|
tokenAccountsByOwner,
|
|
238
238
|
{ includeUnparsed = false } = {}
|
|
239
239
|
) {
|
|
240
|
-
let {
|
|
241
|
-
|
|
242
|
-
preBalances,
|
|
243
|
-
postBalances,
|
|
244
|
-
preTokenBalances,
|
|
245
|
-
postTokenBalances,
|
|
246
|
-
innerInstructions,
|
|
247
|
-
} = txDetails.meta
|
|
240
|
+
let { fee, preBalances, postBalances, preTokenBalances, postTokenBalances, innerInstructions } =
|
|
241
|
+
txDetails.meta
|
|
248
242
|
preBalances = preBalances || []
|
|
249
243
|
postBalances = postBalances || []
|
|
250
244
|
preTokenBalances = preTokenBalances || []
|
|
@@ -383,7 +377,7 @@ export class Api {
|
|
|
383
377
|
const stakeTx = lodash.find(instructions, { program: 'system', type: 'createAccountWithSeed' })
|
|
384
378
|
const stakeWithdraw = lodash.find(instructions, { program: 'stake', type: 'withdraw' })
|
|
385
379
|
const stakeUndelegate = lodash.find(instructions, { program: 'stake', type: 'deactivate' })
|
|
386
|
-
const hasSolanaTx = solanaTx &&
|
|
380
|
+
const hasSolanaTx = solanaTx && preTokenBalances.length === 0 && postTokenBalances.length === 0 // only SOL moved and no tokens movements
|
|
387
381
|
|
|
388
382
|
let tx = {}
|
|
389
383
|
if (hasSolanaTx) {
|
|
@@ -450,7 +444,7 @@ export class Api {
|
|
|
450
444
|
Array.isArray(tokenAccountsByOwner),
|
|
451
445
|
'tokenAccountsByOwner is required when parsing token tx'
|
|
452
446
|
)
|
|
453
|
-
|
|
447
|
+
const tokenTxs = lodash
|
|
454
448
|
.filter(instructions, ({ program, type }) => {
|
|
455
449
|
return program === 'spl-token' && ['transfer', 'transferChecked'].includes(type)
|
|
456
450
|
}) // get Token transfer: could have more than 1 instructions
|
|
@@ -477,7 +471,7 @@ export class Api {
|
|
|
477
471
|
}
|
|
478
472
|
})
|
|
479
473
|
|
|
480
|
-
if (tokenTxs.length) {
|
|
474
|
+
if (tokenTxs.length > 0) {
|
|
481
475
|
// found spl-token simple transfer/transferChecked instruction
|
|
482
476
|
// .reduce to sum/sub (based on isSending) all the same tokens amount (From instructions -> 1 single tx)
|
|
483
477
|
tx = tokenTxs.reduce((finalTx, ix) => {
|
|
@@ -510,10 +504,10 @@ export class Api {
|
|
|
510
504
|
)
|
|
511
505
|
})
|
|
512
506
|
|
|
513
|
-
if (preBalances.length || postBalances.length) {
|
|
507
|
+
if (preBalances.length > 0 || postBalances.length > 0) {
|
|
514
508
|
tx = {}
|
|
515
509
|
|
|
516
|
-
if (includeUnparsed && innerInstructions.length) {
|
|
510
|
+
if (includeUnparsed && innerInstructions.length > 0) {
|
|
517
511
|
// when using includeUnparsed for DEX tx we want to keep SOL tx as "unparsed"
|
|
518
512
|
// 1. we want to treat all SOL dex transactions as "Contract transaction", not "Sent SOL"
|
|
519
513
|
// 2. default behavior is not perfect. For example it doesn't see SOL-side tx in
|
|
@@ -533,7 +527,7 @@ export class Api {
|
|
|
533
527
|
}
|
|
534
528
|
|
|
535
529
|
// If it has inner instructions then it's a DEX tx that moved SPL -> SPL
|
|
536
|
-
if (innerInstructions.length) {
|
|
530
|
+
if (innerInstructions.length > 0) {
|
|
537
531
|
tx.dexTxs = innerInstructions
|
|
538
532
|
// if tx involves only SPL swaps. Expand DEX ix (first element as tx base and the other kept there)
|
|
539
533
|
if (!tx.from && !solanaTx) {
|
|
@@ -573,7 +567,7 @@ export class Api {
|
|
|
573
567
|
|
|
574
568
|
async getWalletTokensList({ tokenAccounts }) {
|
|
575
569
|
const tokensMint = []
|
|
576
|
-
for (
|
|
570
|
+
for (const account of tokenAccounts) {
|
|
577
571
|
const mint = account.mintAddress
|
|
578
572
|
|
|
579
573
|
// skip cached NFT
|
|
@@ -586,6 +580,7 @@ export class Api {
|
|
|
586
580
|
this.tokensToSkip[mint] = true
|
|
587
581
|
continue
|
|
588
582
|
}
|
|
583
|
+
|
|
589
584
|
// OK
|
|
590
585
|
tokensMint.push(mint)
|
|
591
586
|
}
|
|
@@ -601,7 +596,7 @@ export class Api {
|
|
|
601
596
|
)
|
|
602
597
|
|
|
603
598
|
const tokenAccounts = []
|
|
604
|
-
for (
|
|
599
|
+
for (const entry of accountsList) {
|
|
605
600
|
const { pubkey, account } = entry
|
|
606
601
|
|
|
607
602
|
const mint = lodash.get(account, 'data.parsed.info.mint')
|
|
@@ -619,6 +614,7 @@ export class Api {
|
|
|
619
614
|
mintAddress: mint,
|
|
620
615
|
})
|
|
621
616
|
}
|
|
617
|
+
|
|
622
618
|
// eventually filter by token
|
|
623
619
|
return tokenTicker
|
|
624
620
|
? tokenAccounts.filter(({ ticker }) => ticker === tokenTicker)
|
|
@@ -626,18 +622,24 @@ export class Api {
|
|
|
626
622
|
}
|
|
627
623
|
|
|
628
624
|
async getTokensBalance({ address, filterByTokens = [], tokenAccounts }) {
|
|
629
|
-
|
|
625
|
+
const accounts = tokenAccounts || (await this.getTokenAccountsByOwner(address))
|
|
630
626
|
|
|
631
|
-
|
|
632
|
-
if (
|
|
627
|
+
return accounts.reduce((acc, { tokenName, balance }) => {
|
|
628
|
+
if (
|
|
629
|
+
tokenName === 'unknown' ||
|
|
630
|
+
(filterByTokens.length > 0 && !filterByTokens.includes(tokenName))
|
|
631
|
+
)
|
|
633
632
|
return acc // filter by supported tokens only
|
|
634
|
-
if (
|
|
633
|
+
if (acc[tokenName]) {
|
|
634
|
+
acc[tokenName] += Number(balance)
|
|
635
|
+
}
|
|
635
636
|
// e.g { 'serum': 123 }
|
|
636
|
-
else
|
|
637
|
+
else {
|
|
638
|
+
acc[tokenName] = Number(balance)
|
|
639
|
+
} // merge same token account balance
|
|
640
|
+
|
|
637
641
|
return acc
|
|
638
642
|
}, {})
|
|
639
|
-
|
|
640
|
-
return tokensBalance
|
|
641
643
|
}
|
|
642
644
|
|
|
643
645
|
async isAssociatedTokenAccountActive(tokenAddress) {
|
|
@@ -645,7 +647,7 @@ export class Api {
|
|
|
645
647
|
try {
|
|
646
648
|
await this.rpcCall('getTokenAccountBalance', [tokenAddress])
|
|
647
649
|
return true
|
|
648
|
-
} catch
|
|
650
|
+
} catch {
|
|
649
651
|
return false
|
|
650
652
|
}
|
|
651
653
|
}
|
|
@@ -698,20 +700,19 @@ export class Api {
|
|
|
698
700
|
return account.owner === SYSTEM_PROGRAM_ID.toBase58()
|
|
699
701
|
? 'solana'
|
|
700
702
|
: account.owner === TOKEN_PROGRAM_ID.toBase58()
|
|
701
|
-
|
|
702
|
-
|
|
703
|
+
? 'token'
|
|
704
|
+
: null
|
|
703
705
|
}
|
|
704
706
|
|
|
705
707
|
async getTokenAddressOwner(address) {
|
|
706
708
|
const value = await this.getAccountInfo(address)
|
|
707
|
-
|
|
708
|
-
return owner
|
|
709
|
+
return lodash.get(value, 'data.parsed.info.owner', null)
|
|
709
710
|
}
|
|
710
711
|
|
|
711
712
|
async getAddressMint(address) {
|
|
712
713
|
const value = await this.getAccountInfo(address)
|
|
713
|
-
|
|
714
|
-
return
|
|
714
|
+
// token mint
|
|
715
|
+
return lodash.get(value, 'data.parsed.info.mint', null)
|
|
715
716
|
}
|
|
716
717
|
|
|
717
718
|
async isTokenAddress(address) {
|
|
@@ -746,7 +747,7 @@ export class Api {
|
|
|
746
747
|
let locked = 0
|
|
747
748
|
let withdrawable = 0
|
|
748
749
|
let pending = 0
|
|
749
|
-
for (
|
|
750
|
+
for (const entry of res) {
|
|
750
751
|
const addr = entry.pubkey
|
|
751
752
|
const lamports = lodash.get(entry, 'account.lamports', 0)
|
|
752
753
|
const delegation = lodash.get(entry, 'account.data.parsed.info.stake.delegation', {})
|
|
@@ -767,11 +768,12 @@ export class Api {
|
|
|
767
768
|
withdrawable += accounts[addr].canWithdraw ? lamports : 0
|
|
768
769
|
pending += accounts[addr].isDeactivating ? lamports : 0
|
|
769
770
|
}
|
|
771
|
+
|
|
770
772
|
return { accounts, totalStake, locked, withdrawable, pending }
|
|
771
773
|
}
|
|
772
774
|
|
|
773
775
|
async getRewards(stakingAddresses = []) {
|
|
774
|
-
if (
|
|
776
|
+
if (stakingAddresses.length === 0) return 0
|
|
775
777
|
|
|
776
778
|
// custom endpoint!
|
|
777
779
|
const rewards = await this.request('rewards')
|
|
@@ -781,11 +783,9 @@ export class Api {
|
|
|
781
783
|
.json()
|
|
782
784
|
|
|
783
785
|
// sum rewards for all addresses
|
|
784
|
-
|
|
786
|
+
return Object.values(rewards).reduce((total, x) => {
|
|
785
787
|
return total + x
|
|
786
788
|
}, 0)
|
|
787
|
-
|
|
788
|
-
return earnings
|
|
789
789
|
}
|
|
790
790
|
|
|
791
791
|
async getMinimumBalanceForRentExemption(size) {
|
|
@@ -869,6 +869,7 @@ export class Api {
|
|
|
869
869
|
if (error.message && error.message.includes('could not find account')) {
|
|
870
870
|
return defaultValue
|
|
871
871
|
}
|
|
872
|
+
|
|
872
873
|
throw error
|
|
873
874
|
}
|
|
874
875
|
}
|
|
@@ -960,7 +961,7 @@ export class Api {
|
|
|
960
961
|
simulateAndRetrieveSideEffects = async (
|
|
961
962
|
message,
|
|
962
963
|
publicKey,
|
|
963
|
-
transactionMessage
|
|
964
|
+
transactionMessage // decompiled TransactionMessage
|
|
964
965
|
) => {
|
|
965
966
|
const { config, accountAddresses } = getTransactionSimulationParams(
|
|
966
967
|
transactionMessage || message
|
package/src/connection.js
CHANGED
|
@@ -20,7 +20,6 @@ export class Connection {
|
|
|
20
20
|
address,
|
|
21
21
|
tokensAddresses = [],
|
|
22
22
|
callback,
|
|
23
|
-
onMsg,
|
|
24
23
|
reconnectCallback = () => {},
|
|
25
24
|
reconnectDelay = DEFAULT_RECONNECT_DELAY,
|
|
26
25
|
}) {
|
|
@@ -28,7 +27,6 @@ export class Connection {
|
|
|
28
27
|
this.tokensAddresses = tokensAddresses
|
|
29
28
|
this.endpoint = endpoint
|
|
30
29
|
this.callback = callback
|
|
31
|
-
this.onMsg = onMsg
|
|
32
30
|
this.reconnectCallback = reconnectCallback
|
|
33
31
|
this.reconnectDelay = reconnectDelay
|
|
34
32
|
|
|
@@ -69,8 +67,8 @@ export class Connection {
|
|
|
69
67
|
debug('Opening WS to:', reqUrl)
|
|
70
68
|
const ws = new WebSocket(`${reqUrl}`)
|
|
71
69
|
ws.onmessage = this.onMessage.bind(this)
|
|
72
|
-
ws.
|
|
73
|
-
ws.
|
|
70
|
+
ws.addEventListener('open', this.onOpen.bind(this))
|
|
71
|
+
ws.addEventListener('close', this.onClose.bind(this))
|
|
74
72
|
ws.onerror = this.onError.bind(this)
|
|
75
73
|
return ws
|
|
76
74
|
}
|
|
@@ -92,14 +90,14 @@ export class Connection {
|
|
|
92
90
|
}
|
|
93
91
|
|
|
94
92
|
get running() {
|
|
95
|
-
return !!(!this.isClosed || this.inProcessMessages || this.messageQueue.length)
|
|
93
|
+
return !!(!this.isClosed || this.inProcessMessages || this.messageQueue.length > 0)
|
|
96
94
|
}
|
|
97
95
|
|
|
98
96
|
get connectionState() {
|
|
99
97
|
if (this.isConnecting) return 'CONNECTING'
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
98
|
+
if (this.isOpen) return 'OPEN'
|
|
99
|
+
if (this.isClosing) return 'CLOSING'
|
|
100
|
+
if (this.isClosed) return 'CLOSED'
|
|
103
101
|
return 'NONE'
|
|
104
102
|
}
|
|
105
103
|
|
|
@@ -127,7 +125,13 @@ export class Connection {
|
|
|
127
125
|
try {
|
|
128
126
|
const json = JSON.parse(evt.data)
|
|
129
127
|
debug('new ws msg:', json)
|
|
130
|
-
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 {
|
|
131
135
|
if (lodash.get(this.rpcQueue, json.id)) {
|
|
132
136
|
// json-rpc reply
|
|
133
137
|
clearTimeout(this.rpcQueue[json.id].timeout)
|
|
@@ -138,13 +142,8 @@ export class Connection {
|
|
|
138
142
|
debug('pushing msg to queue', msg)
|
|
139
143
|
this.messageQueue.push(msg) // sub results
|
|
140
144
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if (lodash.get(this.rpcQueue, json.id)) {
|
|
144
|
-
this.rpcQueue[json.id].reject(new Error(json.error.message))
|
|
145
|
-
clearTimeout(this.rpcQueue[json.id].timeout)
|
|
146
|
-
delete this.rpcQueue[json.id]
|
|
147
|
-
} else debug('Unsupported WS message:', json.error.message)
|
|
145
|
+
|
|
146
|
+
this.processMessages()
|
|
148
147
|
}
|
|
149
148
|
} catch (e) {
|
|
150
149
|
debug(e)
|
|
@@ -197,12 +196,11 @@ export class Connection {
|
|
|
197
196
|
}
|
|
198
197
|
}
|
|
199
198
|
|
|
200
|
-
async processMessages(
|
|
201
|
-
if (this.onMsg) await this.onMsg(json)
|
|
199
|
+
async processMessages() {
|
|
202
200
|
if (this.inProcessMessages) return null
|
|
203
201
|
this.inProcessMessages = true
|
|
204
202
|
try {
|
|
205
|
-
while (this.messageQueue.length) {
|
|
203
|
+
while (this.messageQueue.length > 0) {
|
|
206
204
|
const items = this.messageQueue.splice(0, this.messageQueue.length)
|
|
207
205
|
await this.callback(items)
|
|
208
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) => {
|
|
@@ -10,14 +10,25 @@ const DEFAULT_REMOTE_CONFIG = {
|
|
|
10
10
|
staking: { enabled: true, pool: DEFAULT_POOL_ADDRESS },
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
const TICKS_BETWEEN_HISTORY_FETCHES = 10
|
|
14
|
+
const TICKS_BETWEEN_STAKE_FETCHES = 5
|
|
15
|
+
|
|
13
16
|
export class SolanaMonitor extends BaseMonitor {
|
|
14
|
-
constructor({
|
|
17
|
+
constructor({
|
|
18
|
+
api,
|
|
19
|
+
includeUnparsed = false,
|
|
20
|
+
ticksBetweenHistoryFetches = TICKS_BETWEEN_HISTORY_FETCHES,
|
|
21
|
+
ticksBetweenStakeFetches = TICKS_BETWEEN_STAKE_FETCHES,
|
|
22
|
+
...args
|
|
23
|
+
}) {
|
|
15
24
|
super(args)
|
|
16
25
|
assert(api, 'api is required')
|
|
17
26
|
this.api = api
|
|
18
27
|
this.cursors = {}
|
|
19
28
|
this.assets = {}
|
|
20
29
|
this.staking = DEFAULT_REMOTE_CONFIG.staking
|
|
30
|
+
this.ticksBetweenStakeFetches = ticksBetweenStakeFetches
|
|
31
|
+
this.ticksBetweenHistoryFetches = ticksBetweenHistoryFetches
|
|
21
32
|
this.includeUnparsed = includeUnparsed
|
|
22
33
|
this.addHook('before-stop', (...args) => this.beforeStop(...args))
|
|
23
34
|
}
|
|
@@ -41,10 +52,15 @@ export class SolanaMonitor extends BaseMonitor {
|
|
|
41
52
|
})
|
|
42
53
|
return this.api.watchAddress({
|
|
43
54
|
address,
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
55
|
+
/*
|
|
56
|
+
// OPTIONAL. Relying on polling through ws
|
|
57
|
+
tokensAddresses: [], // needed for ASA subs
|
|
58
|
+
handleAccounts: (updates) => this.accountsCallback({ updates, walletAccount }),
|
|
59
|
+
handleTransfers: (txs) => {
|
|
60
|
+
// new SOL tx, ticking monitor
|
|
61
|
+
this.tick({ walletAccount }) // it will cause refresh for both sender/receiver. Without necessarily fetching the tx if it's not finalized in the node.
|
|
47
62
|
},
|
|
63
|
+
*/
|
|
48
64
|
})
|
|
49
65
|
}
|
|
50
66
|
|
|
@@ -81,10 +97,24 @@ export class SolanaMonitor extends BaseMonitor {
|
|
|
81
97
|
|
|
82
98
|
async getStakingAddressesFromTxLog({ assetName, walletAccount }) {
|
|
83
99
|
const txLog = await this.aci.getTxLog({ assetName: this.asset.name, walletAccount })
|
|
84
|
-
const stakingAddresses =
|
|
100
|
+
const stakingAddresses = [...txLog]
|
|
85
101
|
.filter((tx) => _.get(tx, 'data.staking.stakeAddresses'))
|
|
86
102
|
.map((tx) => tx.data.staking.stakeAddresses)
|
|
87
|
-
return _.uniq(
|
|
103
|
+
return _.uniq(stakingAddresses.flat())
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
balanceChanged({ account, newAccount }) {
|
|
107
|
+
const solBalanceChanged = !account.balance || !account.balance.equals(newAccount.balance)
|
|
108
|
+
if (solBalanceChanged) return true
|
|
109
|
+
|
|
110
|
+
const tokenBalanceChanged =
|
|
111
|
+
!account.tokenBalances ||
|
|
112
|
+
Object.entries(newAccount.tokenBalances).some(
|
|
113
|
+
([token, balance]) =>
|
|
114
|
+
!account.tokenBalances[token] || !account.tokenBalances[token].equals(balance)
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
return tokenBalanceChanged
|
|
88
118
|
}
|
|
89
119
|
|
|
90
120
|
async tick({ walletAccount, refresh }) {
|
|
@@ -99,28 +129,43 @@ export class SolanaMonitor extends BaseMonitor {
|
|
|
99
129
|
const address = await this.aci.getReceiveAddress({ assetName, walletAccount, useCache: true })
|
|
100
130
|
const stakingAddresses = await this.getStakingAddressesFromTxLog({ assetName, walletAccount })
|
|
101
131
|
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
refresh,
|
|
107
|
-
})
|
|
132
|
+
const fetchStakingInfo = this.tickCount[walletAccount] % this.ticksBetweenStakeFetches === 0
|
|
133
|
+
const staking = fetchStakingInfo
|
|
134
|
+
? await this.getStakingInfo({ address, stakingAddresses })
|
|
135
|
+
: accountState.mem
|
|
108
136
|
|
|
109
|
-
const
|
|
137
|
+
const tokenAccounts = await this.api.getTokenAccountsByOwner(address)
|
|
138
|
+
const account = await this.getAccount({ address, staking, tokenAccounts })
|
|
110
139
|
|
|
111
|
-
|
|
112
|
-
const staking =
|
|
113
|
-
refresh || cursorChanged
|
|
114
|
-
? await this.getStakingInfo({ address, stakingAddresses })
|
|
115
|
-
: accountState.mem
|
|
140
|
+
const balanceChanged = this.balanceChanged({ account: accountState, newAccount: account })
|
|
116
141
|
|
|
117
|
-
|
|
118
|
-
|
|
142
|
+
const isHistoryUpdateTick =
|
|
143
|
+
this.tickCount[walletAccount] % this.ticksBetweenHistoryFetches === 0
|
|
119
144
|
|
|
145
|
+
const shouldUpdateHistory = refresh || isHistoryUpdateTick || balanceChanged
|
|
146
|
+
const shouldUpdateOnlyBalance = balanceChanged && !shouldUpdateHistory
|
|
147
|
+
const shouldUpdateBalanceBeforeHistory = true
|
|
148
|
+
|
|
149
|
+
// getHistory is more likely to fail/be rate limited, so we want to update users balance only on a lot of ticks
|
|
150
|
+
if (shouldUpdateBalanceBeforeHistory || shouldUpdateOnlyBalance) {
|
|
120
151
|
// update all state at once
|
|
152
|
+
await this.updateState({ account, walletAccount, staking })
|
|
121
153
|
await this.emitUnknownTokensEvent({ tokenAccounts })
|
|
154
|
+
}
|
|
155
|
+
if (shouldUpdateHistory) {
|
|
156
|
+
const { logItemsByAsset, cursorState } = await this.getHistory({
|
|
157
|
+
address,
|
|
158
|
+
accountState,
|
|
159
|
+
walletAccount,
|
|
160
|
+
refresh,
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
const cursorChanged = this.hasNewCursor({ walletAccount, cursorState })
|
|
164
|
+
|
|
165
|
+
// update all state at once
|
|
122
166
|
await this.updateTxLogByAsset({ walletAccount, logItemsByAsset, refresh })
|
|
123
167
|
await this.updateState({ account, cursorState, walletAccount, staking })
|
|
168
|
+
await this.emitUnknownTokensEvent({ tokenAccounts })
|
|
124
169
|
if (refresh || cursorChanged) {
|
|
125
170
|
this.cursors[walletAccount] = cursorState.cursor
|
|
126
171
|
}
|
|
@@ -128,7 +173,7 @@ export class SolanaMonitor extends BaseMonitor {
|
|
|
128
173
|
}
|
|
129
174
|
|
|
130
175
|
async getHistory({ address, accountState, refresh } = {}) {
|
|
131
|
-
|
|
176
|
+
const cursor = refresh ? '' : accountState.cursor
|
|
132
177
|
const baseAsset = this.asset
|
|
133
178
|
|
|
134
179
|
const { transactions, newCursor } = await this.api.getTransactions(address, {
|
|
@@ -181,6 +226,7 @@ export class SolanaMonitor extends BaseMonitor {
|
|
|
181
226
|
|
|
182
227
|
item.data.meta = tx.data.meta
|
|
183
228
|
}
|
|
229
|
+
|
|
184
230
|
if (asset.assetType === 'SOLANA_TOKEN' && item.feeAmount && item.feeAmount.isPositive) {
|
|
185
231
|
const feeItem = {
|
|
186
232
|
..._.clone(item),
|
|
@@ -190,6 +236,7 @@ export class SolanaMonitor extends BaseMonitor {
|
|
|
190
236
|
}
|
|
191
237
|
mappedTransactions.push(feeItem)
|
|
192
238
|
}
|
|
239
|
+
|
|
193
240
|
mappedTransactions.push(item)
|
|
194
241
|
}
|
|
195
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
|
+
}
|