@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.
Files changed (44) hide show
  1. package/README.md +20 -0
  2. package/package.json +53 -0
  3. package/src/api.js +198 -0
  4. package/src/balances.js +22 -0
  5. package/src/basic-account-state.js +11 -0
  6. package/src/create-unsigned-tx.js +60 -0
  7. package/src/get-fee-async.js +62 -0
  8. package/src/get-fee.js +10 -0
  9. package/src/index.js +9 -0
  10. package/src/sui-no-history-monitor.js +277 -0
  11. package/src/transform-tx.js +100 -0
  12. package/src/tx-send.js +249 -0
  13. package/src/vendors/client/client.js +891 -0
  14. package/src/vendors/client/errors.js +38 -0
  15. package/src/vendors/client/http-transport.js +108 -0
  16. package/src/vendors/client/index.js +1 -0
  17. package/src/vendors/client/rpc-websocket-client.js +178 -0
  18. package/src/vendors/experimental/cache.js +89 -0
  19. package/src/vendors/experimental/client.js +27 -0
  20. package/src/vendors/experimental/core.js +105 -0
  21. package/src/vendors/experimental/errors.js +32 -0
  22. package/src/vendors/experimental/index.js +5 -0
  23. package/src/vendors/experimental/mvr.js +443 -0
  24. package/src/vendors/experimental/transports/json-rpc-resolver.js +359 -0
  25. package/src/vendors/experimental/transports/jsonRPC.js +504 -0
  26. package/src/vendors/experimental/transports/utils.js +90 -0
  27. package/src/vendors/transactions/Arguments.js +22 -0
  28. package/src/vendors/transactions/Commands.js +108 -0
  29. package/src/vendors/transactions/Inputs.js +59 -0
  30. package/src/vendors/transactions/ObjectCache.js +272 -0
  31. package/src/vendors/transactions/Transaction.js +843 -0
  32. package/src/vendors/transactions/TransactionData.js +293 -0
  33. package/src/vendors/transactions/data/internal.js +218 -0
  34. package/src/vendors/transactions/data/v1.js +538 -0
  35. package/src/vendors/transactions/data/v2.js +119 -0
  36. package/src/vendors/transactions/hash.js +12 -0
  37. package/src/vendors/transactions/index.js +1 -0
  38. package/src/vendors/transactions/object.js +22 -0
  39. package/src/vendors/transactions/plugins/NamedPackagesPlugin.js +49 -0
  40. package/src/vendors/transactions/pure.js +40 -0
  41. package/src/vendors/transactions/resolve.js +95 -0
  42. package/src/vendors/transactions/serializer.js +203 -0
  43. package/src/vendors/transactions/utils.js +67 -0
  44. 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
+ }