@exodus/sui-api 0.1.0
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/README.md +20 -0
- package/package.json +53 -0
- package/src/api.js +198 -0
- package/src/balances.js +22 -0
- package/src/basic-account-state.js +11 -0
- package/src/create-unsigned-tx.js +60 -0
- package/src/get-fee-async.js +62 -0
- package/src/get-fee.js +10 -0
- package/src/index.js +9 -0
- package/src/sui-no-history-monitor.js +277 -0
- package/src/transform-tx.js +100 -0
- package/src/tx-send.js +249 -0
- package/src/vendors/client/client.js +891 -0
- package/src/vendors/client/errors.js +38 -0
- package/src/vendors/client/http-transport.js +108 -0
- package/src/vendors/client/index.js +1 -0
- package/src/vendors/client/rpc-websocket-client.js +178 -0
- package/src/vendors/experimental/cache.js +89 -0
- package/src/vendors/experimental/client.js +27 -0
- package/src/vendors/experimental/core.js +105 -0
- package/src/vendors/experimental/errors.js +32 -0
- package/src/vendors/experimental/index.js +5 -0
- package/src/vendors/experimental/mvr.js +443 -0
- package/src/vendors/experimental/transports/json-rpc-resolver.js +359 -0
- package/src/vendors/experimental/transports/jsonRPC.js +504 -0
- package/src/vendors/experimental/transports/utils.js +90 -0
- package/src/vendors/transactions/Arguments.js +22 -0
- package/src/vendors/transactions/Commands.js +108 -0
- package/src/vendors/transactions/Inputs.js +59 -0
- package/src/vendors/transactions/ObjectCache.js +272 -0
- package/src/vendors/transactions/Transaction.js +843 -0
- package/src/vendors/transactions/TransactionData.js +293 -0
- package/src/vendors/transactions/data/internal.js +218 -0
- package/src/vendors/transactions/data/v1.js +538 -0
- package/src/vendors/transactions/data/v2.js +119 -0
- package/src/vendors/transactions/hash.js +12 -0
- package/src/vendors/transactions/index.js +1 -0
- package/src/vendors/transactions/object.js +22 -0
- package/src/vendors/transactions/plugins/NamedPackagesPlugin.js +49 -0
- package/src/vendors/transactions/pure.js +40 -0
- package/src/vendors/transactions/resolve.js +95 -0
- package/src/vendors/transactions/serializer.js +203 -0
- package/src/vendors/transactions/utils.js +67 -0
- package/src/vendors/valibot.js +7920 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { BaseMonitor } from '@exodus/asset-lib'
|
|
2
|
+
import { SynchronizedTime } from '@exodus/basic-utils'
|
|
3
|
+
import { Address, UtxoCollection } from '@exodus/models'
|
|
4
|
+
import { coinTypes, RPC_URL } from '@exodus/sui-lib'
|
|
5
|
+
import lodash from 'lodash'
|
|
6
|
+
import assert from 'minimalistic-assert'
|
|
7
|
+
import ms from 'ms'
|
|
8
|
+
|
|
9
|
+
import { transformTransactions } from './transform-tx.js'
|
|
10
|
+
|
|
11
|
+
const { isEmpty, unionBy, zipObject } = lodash
|
|
12
|
+
|
|
13
|
+
export const UNCONFIRMED_TX_LIMIT = ms('5m')
|
|
14
|
+
|
|
15
|
+
export class SuiNoHistoryMonitor extends BaseMonitor {
|
|
16
|
+
#serverApi
|
|
17
|
+
|
|
18
|
+
constructor({ asset, serverApi, interval = ms('15s'), assetClientInterface, runner, logger }) {
|
|
19
|
+
super({
|
|
20
|
+
asset,
|
|
21
|
+
interval,
|
|
22
|
+
assetClientInterface,
|
|
23
|
+
logger,
|
|
24
|
+
runner,
|
|
25
|
+
})
|
|
26
|
+
assert(serverApi, 'Sui Node API Client is required!')
|
|
27
|
+
|
|
28
|
+
this.#serverApi = serverApi
|
|
29
|
+
this.assets = Object.create(null)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// public methods
|
|
33
|
+
|
|
34
|
+
setServer(remoteConfig) {
|
|
35
|
+
this.#serverApi.setServer({
|
|
36
|
+
rpcUrl: remoteConfig?.rpcUrl || RPC_URL,
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async tick({ refresh, walletAccount }) {
|
|
41
|
+
const assetName = this.asset.name
|
|
42
|
+
this.assets = await this.aci.getAssetsForNetwork({ baseAssetName: assetName })
|
|
43
|
+
|
|
44
|
+
const ourWalletAddress = await this.aci.getReceiveAddress({ assetName, walletAccount })
|
|
45
|
+
|
|
46
|
+
const { utxos, tokenUtxos, unknownAssetIds } = await this.#getNewAccountState({
|
|
47
|
+
address: ourWalletAddress,
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
if (unknownAssetIds.length > 0) {
|
|
51
|
+
this.emit('unknown-tokens', unknownAssetIds)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let batch = this.aci.createOperationsBatch()
|
|
55
|
+
|
|
56
|
+
batch = await this.aci.updateAccountStateBatch({
|
|
57
|
+
assetName,
|
|
58
|
+
walletAccount,
|
|
59
|
+
newData: {
|
|
60
|
+
utxos,
|
|
61
|
+
tokenUtxos,
|
|
62
|
+
},
|
|
63
|
+
batch,
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
const { txsToRemoveByAsset, txLogItemsByAsset } = await this.#resolvePendingTransactions({
|
|
67
|
+
assets: this.assets,
|
|
68
|
+
walletAccount,
|
|
69
|
+
ourWalletAddress,
|
|
70
|
+
refresh,
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
for (const [assetName, txsToRemove] of Object.entries(txsToRemoveByAsset)) {
|
|
74
|
+
batch = await this.aci.removeTxLogBatch({ assetName, walletAccount, txs: txsToRemove, batch })
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
for (const [assetName, txLogItems] of Object.entries(txLogItemsByAsset)) {
|
|
78
|
+
batch = await this.aci.updateTxLogAndNotifyBatch({
|
|
79
|
+
assetName,
|
|
80
|
+
walletAccount,
|
|
81
|
+
txs: txLogItems,
|
|
82
|
+
batch,
|
|
83
|
+
refresh,
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
await this.aci.executeOperationsBatch(batch)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
#checkPendingTransactions = ({ pendingTransactions, pendingTxsFromNode }) => {
|
|
91
|
+
const txsToUpdate = []
|
|
92
|
+
const txsToRemove = []
|
|
93
|
+
const now = SynchronizedTime.now()
|
|
94
|
+
|
|
95
|
+
if (isEmpty(pendingTransactions)) {
|
|
96
|
+
return {
|
|
97
|
+
txsToUpdate,
|
|
98
|
+
txsToRemove,
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
for (const tx of pendingTransactions) {
|
|
103
|
+
const txFromNode = pendingTxsFromNode[tx.txId]
|
|
104
|
+
if (txFromNode) {
|
|
105
|
+
if (txFromNode.error === false) {
|
|
106
|
+
txsToUpdate.push({ ...tx, error: false, confirmations: 1 })
|
|
107
|
+
} else if (txFromNode.error === true) {
|
|
108
|
+
txsToUpdate.push({ ...tx, confirmations: 1 })
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
if (now - tx.date.getTime() > UNCONFIRMED_TX_LIMIT) {
|
|
112
|
+
txsToRemove.push(tx)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
txsToUpdate,
|
|
119
|
+
txsToRemove,
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
#getTransactionsFromNode = async ({ digests, ourWalletAddress, asset }) => {
|
|
124
|
+
if (digests.length === 0) return []
|
|
125
|
+
|
|
126
|
+
const transactions = await this.#serverApi.getTransactions({
|
|
127
|
+
txIds: digests,
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
const parsedTxs = transformTransactions({
|
|
131
|
+
transactionResults: transactions,
|
|
132
|
+
address: ourWalletAddress,
|
|
133
|
+
feeAsset: asset.feeAsset,
|
|
134
|
+
asset,
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
const txIds = parsedTxs.map(({ txId }) => txId)
|
|
138
|
+
|
|
139
|
+
return zipObject(txIds, parsedTxs)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
#resolvePendingTransactions = async ({ assets, walletAccount, ourWalletAddress }) => {
|
|
143
|
+
const txsToRemoveByAsset = Object.create(null)
|
|
144
|
+
const txLogItemsByAsset = Object.create(null)
|
|
145
|
+
|
|
146
|
+
for (const [assetName, asset] of Object.entries(assets)) {
|
|
147
|
+
const { pendingTransactions } = await this.#getPendingTransactionsByAsset({
|
|
148
|
+
assetName,
|
|
149
|
+
walletAccount,
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
if (pendingTransactions.length === 0) continue
|
|
153
|
+
|
|
154
|
+
const pendingTxsFromNode = await this.#getTransactionsFromNode({
|
|
155
|
+
digests: pendingTransactions.map((tx) => tx.txId),
|
|
156
|
+
asset,
|
|
157
|
+
ourWalletAddress,
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
const { txsToUpdate: logItems, txsToRemove } = await this.#checkPendingTransactions({
|
|
161
|
+
pendingTransactions,
|
|
162
|
+
pendingTxsFromNode,
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
if (txsToRemove.length > 0) {
|
|
166
|
+
txsToRemoveByAsset[assetName] = txsToRemove
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (logItems.length > 0) {
|
|
170
|
+
txLogItemsByAsset[assetName] = logItems
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return { txsToRemoveByAsset, txLogItemsByAsset }
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
#getPendingTransactionsByAsset = async ({ assetName, walletAccount }) => {
|
|
178
|
+
const { stale, unconfirmed } = await this.#deriveTransactionsToCheck({
|
|
179
|
+
assetName,
|
|
180
|
+
walletAccount,
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
const pendingTransactions = unionBy(Object.values(stale), Object.values(unconfirmed), 'tx.txId')
|
|
184
|
+
|
|
185
|
+
return { pendingTransactions }
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
#deriveTransactionsToCheck = async ({ assetName, walletAccount }) => {
|
|
189
|
+
const txLog = await this.aci.getTxLog({ assetName, walletAccount })
|
|
190
|
+
const { stale, unconfirmed } = this.getUnconfirmed({
|
|
191
|
+
txSet: txLog,
|
|
192
|
+
staleTxAge: UNCONFIRMED_TX_LIMIT,
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
stale: stale.map((tx) => txLog.get(tx.txId)).filter(Boolean),
|
|
197
|
+
unconfirmed: unconfirmed.map((txId) => txLog.get(txId)).filter(Boolean),
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
#getNewAccountState = async ({ address }) => {
|
|
202
|
+
const allCoins = await this.#serverApi.getAllCoins({
|
|
203
|
+
owner: address,
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
const groupedCoins = allCoins.reduce((acc, coin) => {
|
|
207
|
+
if (!acc[coin.coinType]) {
|
|
208
|
+
acc[coin.coinType] = []
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
acc[coin.coinType].push(coin)
|
|
212
|
+
return acc
|
|
213
|
+
}, Object.create(null))
|
|
214
|
+
|
|
215
|
+
const tokenUtxos = Object.create(null)
|
|
216
|
+
let baseUtxos = UtxoCollection.createEmpty({
|
|
217
|
+
currency: this.asset.currency,
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
const unknownAssetIds = new Set(
|
|
221
|
+
Object.keys(groupedCoins).filter((coinType) => coinType !== coinTypes.Base)
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
for (const [assetName, asset] of Object.entries(this.assets)) {
|
|
225
|
+
const isBaseAsset = asset.baseAssetName === asset.name
|
|
226
|
+
if (isBaseAsset) {
|
|
227
|
+
baseUtxos = this.#formatUtxos({
|
|
228
|
+
address,
|
|
229
|
+
coinType: coinTypes.Base,
|
|
230
|
+
coins: groupedCoins[coinTypes.Base] || [],
|
|
231
|
+
asset,
|
|
232
|
+
})
|
|
233
|
+
continue
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (!unknownAssetIds.delete(asset.assetId)) continue
|
|
237
|
+
|
|
238
|
+
tokenUtxos[assetName] = this.#formatUtxos({
|
|
239
|
+
address,
|
|
240
|
+
coinType: asset.assetId,
|
|
241
|
+
coins: groupedCoins[asset.assetId] || [],
|
|
242
|
+
asset,
|
|
243
|
+
})
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
utxos: baseUtxos,
|
|
248
|
+
tokenUtxos,
|
|
249
|
+
unknownAssetIds: [...unknownAssetIds],
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
#formatUtxos = ({ coinType, coins, asset, address }) => {
|
|
254
|
+
if (coins.length === 0) {
|
|
255
|
+
return UtxoCollection.createEmpty({
|
|
256
|
+
currency: asset.currency,
|
|
257
|
+
})
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const utxoItems = coins.map((coin) => {
|
|
261
|
+
return {
|
|
262
|
+
address: Address.create(address, { path: 'm/0/0' }),
|
|
263
|
+
txId: coin.previousTransaction,
|
|
264
|
+
digest: coin.digest,
|
|
265
|
+
coinObjectId: coin.coinObjectId,
|
|
266
|
+
version: coin.version,
|
|
267
|
+
coinType,
|
|
268
|
+
confirmations: 1,
|
|
269
|
+
value: asset.currency.baseUnit(coin.balance),
|
|
270
|
+
}
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
return UtxoCollection.fromArray(utxoItems, { currency: asset.currency })
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export const createSuiNoHistoryMonitor = (args) => new SuiNoHistoryMonitor(args)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
const parseTransaction = ({ transaction, asset }) => {
|
|
2
|
+
const { data } = transaction
|
|
3
|
+
const {
|
|
4
|
+
sender,
|
|
5
|
+
transaction: { inputs },
|
|
6
|
+
} = data
|
|
7
|
+
|
|
8
|
+
const recipient = inputs.find(
|
|
9
|
+
(input) => input.type === 'pure' && input.valueType === 'address'
|
|
10
|
+
)?.value
|
|
11
|
+
|
|
12
|
+
const amount = inputs.find((input) => input.type === 'pure' && input.valueType === 'u64')?.value
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
from: sender,
|
|
16
|
+
to: recipient,
|
|
17
|
+
coinAmount: amount ? asset.currency.baseUnit(amount) : asset.currency.ZERO,
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const getGasBudget = ({ gasUsed, feeAsset }) => {
|
|
22
|
+
// gas_budget >= max{computation_fees,total_gas_fees}
|
|
23
|
+
|
|
24
|
+
const { currency } = feeAsset
|
|
25
|
+
|
|
26
|
+
const totalFee = getFeeAmount({ gasUsed, feeAsset })
|
|
27
|
+
|
|
28
|
+
const computationAndStorageFee = currency
|
|
29
|
+
.baseUnit(gasUsed.computationCost)
|
|
30
|
+
.add(currency.baseUnit(gasUsed.storageCost))
|
|
31
|
+
|
|
32
|
+
if (computationAndStorageFee.gt(totalFee)) {
|
|
33
|
+
return computationAndStorageFee
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return totalFee
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const getFeeAmount = ({ gasUsed, feeAsset }) => {
|
|
40
|
+
const { currency } = feeAsset
|
|
41
|
+
return currency
|
|
42
|
+
.baseUnit(gasUsed.computationCost)
|
|
43
|
+
.add(currency.baseUnit(gasUsed.storageCost))
|
|
44
|
+
.sub(currency.baseUnit(gasUsed.storageRebate))
|
|
45
|
+
.clampLowerZero()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const parseConfirmations = ({ effects }) => {
|
|
49
|
+
const { status, gasObject } = effects
|
|
50
|
+
|
|
51
|
+
if (!status?.status || !gasObject?.reference?.version) {
|
|
52
|
+
return {
|
|
53
|
+
confirmations: 0,
|
|
54
|
+
error: 0,
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
confirmations: 1,
|
|
60
|
+
error: status.status === 'failure' ? status.error : false,
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function transformTransactions({ transactionResults, address, feeAsset, asset }) {
|
|
65
|
+
if (transactionResults.length === 0) return []
|
|
66
|
+
return transactionResults.map((transactionResult) => {
|
|
67
|
+
const { effects, transaction, timestampMs } = transactionResult
|
|
68
|
+
const { gasUsed } = effects
|
|
69
|
+
|
|
70
|
+
const { from, to, coinAmount } = parseTransaction({ transaction, asset })
|
|
71
|
+
const { confirmations, error } = parseConfirmations({ effects })
|
|
72
|
+
const wasSend = from === address
|
|
73
|
+
const args = Object.create(null)
|
|
74
|
+
|
|
75
|
+
if (wasSend) {
|
|
76
|
+
args.to = to
|
|
77
|
+
args.feeAmount = getFeeAmount({ gasUsed, feeAsset })
|
|
78
|
+
args.feeCoinName = feeAsset.name
|
|
79
|
+
args.coinAmount = coinAmount.negate()
|
|
80
|
+
} else {
|
|
81
|
+
args.from = [from]
|
|
82
|
+
args.coinAmount = coinAmount
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
txId: transactionResult.digest,
|
|
87
|
+
date: new Date(parseInt(timestampMs, 10)),
|
|
88
|
+
selfSend: wasSend && from === to,
|
|
89
|
+
confirmations,
|
|
90
|
+
error,
|
|
91
|
+
data: Object.create(null),
|
|
92
|
+
currencies: {
|
|
93
|
+
[asset.name]: asset.currency,
|
|
94
|
+
[feeAsset.name]: feeAsset.currency,
|
|
95
|
+
},
|
|
96
|
+
coinName: asset.name,
|
|
97
|
+
...args,
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
}
|
package/src/tx-send.js
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { UtxoCollection } from '@exodus/models'
|
|
2
|
+
import { coinTypes, objectTypes, toCoinType } from '@exodus/sui-lib'
|
|
3
|
+
import assert from 'minimalistic-assert'
|
|
4
|
+
|
|
5
|
+
import { getFeeAmount } from './transform-tx.js'
|
|
6
|
+
|
|
7
|
+
const deriveNewUtxos = ({ mutatedObject, balanceChange, deletedObjects, utxos, asset }) => {
|
|
8
|
+
const utxoArray = utxos.toArray()
|
|
9
|
+
|
|
10
|
+
const deletedObjectIds = new Set(deletedObjects.map(({ objectId }) => objectId))
|
|
11
|
+
const deleted = utxoArray.filter((utxo) => deletedObjectIds.has(utxo.coinObjectId))
|
|
12
|
+
|
|
13
|
+
const deletedBalance = deleted.reduce((acc, utxo) => {
|
|
14
|
+
return acc.add(utxo.value)
|
|
15
|
+
}, asset.currency.baseUnit(0))
|
|
16
|
+
|
|
17
|
+
const mutated = utxoArray.find((utxo) => utxo.coinObjectId === mutatedObject.objectId)
|
|
18
|
+
|
|
19
|
+
const newMutatedBalance = mutated.value
|
|
20
|
+
.add(deletedBalance)
|
|
21
|
+
.add(asset.currency.baseUnit(balanceChange))
|
|
22
|
+
|
|
23
|
+
const newMutated = {
|
|
24
|
+
...mutated,
|
|
25
|
+
version: mutatedObject.version,
|
|
26
|
+
digest: mutatedObject.digest,
|
|
27
|
+
value: newMutatedBalance,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const unaffectedObjects = utxoArray.filter(
|
|
31
|
+
(utxo) =>
|
|
32
|
+
utxo.coinObjectId !== mutatedObject.objectId && !deletedObjectIds.has(utxo.coinObjectId)
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
utxos: UtxoCollection.fromArray([...unaffectedObjects, newMutated], {
|
|
37
|
+
currency: asset.currency,
|
|
38
|
+
}),
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const getObjectAndBalanceChangesByType = ({
|
|
43
|
+
objectChanges,
|
|
44
|
+
balanceChanges,
|
|
45
|
+
ownerAddress,
|
|
46
|
+
asset,
|
|
47
|
+
}) => {
|
|
48
|
+
const ownerBalanceChangesByType = balanceChanges.reduce((acc, change) => {
|
|
49
|
+
const { owner, coinType, amount } = change
|
|
50
|
+
if (owner?.AddressOwner !== ownerAddress) {
|
|
51
|
+
return acc
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
acc[coinType] = amount
|
|
55
|
+
|
|
56
|
+
return acc
|
|
57
|
+
}, Object.create(null))
|
|
58
|
+
|
|
59
|
+
const wasBaseAssetSent = asset.name === asset.baseAssetName
|
|
60
|
+
|
|
61
|
+
const ownerObjectChangesByType = objectChanges.reduce((acc, change) => {
|
|
62
|
+
const { owner, objectType, type } = change
|
|
63
|
+
|
|
64
|
+
if (!acc[objectType]) {
|
|
65
|
+
acc[objectType] = {
|
|
66
|
+
created: [],
|
|
67
|
+
mutated: [],
|
|
68
|
+
deleted: [],
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
switch (type) {
|
|
73
|
+
case 'mutated':
|
|
74
|
+
if (owner?.AddressOwner !== ownerAddress) {
|
|
75
|
+
return acc
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
acc[objectType].mutated.push(change)
|
|
79
|
+
break
|
|
80
|
+
case 'created':
|
|
81
|
+
if (owner?.AddressOwner !== ownerAddress) {
|
|
82
|
+
return acc
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
acc[objectType].created.push(change)
|
|
86
|
+
break
|
|
87
|
+
case 'deleted':
|
|
88
|
+
acc[objectType].deleted.push(change)
|
|
89
|
+
break
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return acc
|
|
93
|
+
}, Object.create(null))
|
|
94
|
+
|
|
95
|
+
const baseObjectsMutated = ownerObjectChangesByType[objectTypes.Base]?.mutated
|
|
96
|
+
|
|
97
|
+
if (
|
|
98
|
+
!baseObjectsMutated ||
|
|
99
|
+
baseObjectsMutated.length !== 1 ||
|
|
100
|
+
!ownerBalanceChangesByType[coinTypes.Base]
|
|
101
|
+
) {
|
|
102
|
+
// expect to have 1 mutated object at point because coins were merged
|
|
103
|
+
throw new Error('In-decidable state change')
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!wasBaseAssetSent) {
|
|
107
|
+
const assetId = asset.assetId
|
|
108
|
+
const coinType = toCoinType(assetId)
|
|
109
|
+
|
|
110
|
+
const assetObjectsMutated = ownerObjectChangesByType[coinType]?.mutated
|
|
111
|
+
|
|
112
|
+
if (
|
|
113
|
+
!assetObjectsMutated ||
|
|
114
|
+
assetObjectsMutated.length !== 1 ||
|
|
115
|
+
!ownerBalanceChangesByType[assetId]
|
|
116
|
+
) {
|
|
117
|
+
// expect to have 1 mutated object at point because coins were merged
|
|
118
|
+
throw new Error('In-decidable state change')
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
ownerObjectChangesByType,
|
|
124
|
+
ownerBalanceChangesByType,
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const updateAccountStateAfterSend = async ({
|
|
129
|
+
aci,
|
|
130
|
+
asset,
|
|
131
|
+
walletAccount,
|
|
132
|
+
ownerAddress,
|
|
133
|
+
result,
|
|
134
|
+
amount,
|
|
135
|
+
recipientAddress,
|
|
136
|
+
}) => {
|
|
137
|
+
const { feeAsset, currency: assetCurrency, name: assetName } = asset
|
|
138
|
+
const { objectChanges, balanceChanges, digest, effects } = result
|
|
139
|
+
const { gasUsed } = effects
|
|
140
|
+
const wasBaseAssetSent = assetName === asset.baseAssetName
|
|
141
|
+
|
|
142
|
+
const fee = getFeeAmount({ gasUsed, feeAsset })
|
|
143
|
+
|
|
144
|
+
const accountState = await aci.getAccountState({
|
|
145
|
+
assetName: asset.baseAssetName,
|
|
146
|
+
walletAccount,
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
const tx = {
|
|
150
|
+
txId: digest,
|
|
151
|
+
confirmations: 0,
|
|
152
|
+
coinName: assetName,
|
|
153
|
+
coinAmount: amount.abs().negate(),
|
|
154
|
+
feeAmount: fee,
|
|
155
|
+
feeCoinName: feeAsset.name,
|
|
156
|
+
selfSend: false,
|
|
157
|
+
to: recipientAddress,
|
|
158
|
+
data: Object.create(null),
|
|
159
|
+
currencies: { [assetName]: assetCurrency, [feeAsset.name]: feeAsset.currency },
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const { ownerObjectChangesByType, ownerBalanceChangesByType } = getObjectAndBalanceChangesByType({
|
|
163
|
+
objectChanges,
|
|
164
|
+
balanceChanges,
|
|
165
|
+
ownerAddress,
|
|
166
|
+
asset,
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
const newAccountState = Object.create(null)
|
|
170
|
+
|
|
171
|
+
const newBaseUtxos = deriveNewUtxos({
|
|
172
|
+
mutatedObject: ownerObjectChangesByType[objectTypes.Base].mutated[0],
|
|
173
|
+
balanceChange: ownerBalanceChangesByType[coinTypes.Base],
|
|
174
|
+
deletedObjects: ownerObjectChangesByType[objectTypes.Base].deleted,
|
|
175
|
+
utxos: accountState.utxos,
|
|
176
|
+
asset: asset.baseAsset,
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
newAccountState.utxos = newBaseUtxos.utxos
|
|
180
|
+
|
|
181
|
+
if (!wasBaseAssetSent) {
|
|
182
|
+
const coinType = toCoinType(asset.assetId)
|
|
183
|
+
const newTokenUtxos = deriveNewUtxos({
|
|
184
|
+
mutatedObject: ownerObjectChangesByType[coinType].mutated[0],
|
|
185
|
+
balanceChange: ownerBalanceChangesByType[asset.assetId],
|
|
186
|
+
deletedObjects: ownerObjectChangesByType[coinType].deleted,
|
|
187
|
+
utxos: accountState.tokenUtxos[asset.name],
|
|
188
|
+
asset,
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
newAccountState.tokenUtxos = { [asset.name]: newTokenUtxos.utxos }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
let batch = aci.createOperationsBatch()
|
|
195
|
+
|
|
196
|
+
batch = await aci.updateAccountStateBatch({
|
|
197
|
+
assetName: asset.baseAssetName,
|
|
198
|
+
walletAccount,
|
|
199
|
+
newData: {
|
|
200
|
+
utxos: newAccountState.utxos,
|
|
201
|
+
tokenUtxos: {
|
|
202
|
+
...accountState.tokenUtxos,
|
|
203
|
+
...(newAccountState.tokenUtxos && newAccountState.tokenUtxos[asset.name]
|
|
204
|
+
? { [asset.name]: newAccountState.tokenUtxos[asset.name] }
|
|
205
|
+
: Object.create(null)),
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
batch,
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
batch = await aci.updateTxLogAndNotifyBatch({ assetName, walletAccount, txs: [tx], batch })
|
|
212
|
+
|
|
213
|
+
await aci.executeOperationsBatch(batch)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export const txSendFactory = ({ assetClientInterface: aci, serverApi, createUnsignedTx }) => {
|
|
217
|
+
assert(aci, 'assetClientInterface is required')
|
|
218
|
+
|
|
219
|
+
return async ({ asset, walletAccount, address: recipientAddress, amount, gasBudget }) => {
|
|
220
|
+
const { name: assetName } = asset
|
|
221
|
+
|
|
222
|
+
const senderAddress = await aci.getReceiveAddress({ assetName, walletAccount })
|
|
223
|
+
|
|
224
|
+
const unsignedTx = await createUnsignedTx({
|
|
225
|
+
asset,
|
|
226
|
+
senderAddress,
|
|
227
|
+
recipientAddress,
|
|
228
|
+
amount: amount.toBaseString(),
|
|
229
|
+
serverApi,
|
|
230
|
+
gasBudget,
|
|
231
|
+
walletAccount,
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
const { plainTx } = await aci.signTransaction({ assetName, unsignedTx, walletAccount })
|
|
235
|
+
|
|
236
|
+
const result = await serverApi.broadcastTx(plainTx)
|
|
237
|
+
|
|
238
|
+
await updateAccountStateAfterSend({
|
|
239
|
+
aci,
|
|
240
|
+
asset,
|
|
241
|
+
walletAccount,
|
|
242
|
+
ownerAddress: senderAddress,
|
|
243
|
+
result,
|
|
244
|
+
amount,
|
|
245
|
+
recipientAddress,
|
|
246
|
+
})
|
|
247
|
+
return { txId: result.digest }
|
|
248
|
+
}
|
|
249
|
+
}
|