@exodus/sui-api 1.4.0 → 1.5.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/CHANGELOG.md +18 -0
- package/package.json +5 -5
- package/src/address-balance-spend.js +48 -0
- package/src/api.js +18 -2
- package/src/balances.js +11 -11
- package/src/basic-account-state.js +1 -0
- package/src/create-unsigned-tx.js +94 -21
- package/src/get-fee-async.js +14 -1
- package/src/graphql/json-rpc-shapes.js +1 -1
- package/src/graphql/queries.js +2 -1
- package/src/staking/create-staking-api.js +21 -21
- package/src/sui-no-history-monitor.js +32 -19
- package/src/transform-tx.js +4 -1
- package/src/tx-send.js +138 -44
- package/src/vendors/transactions/Inputs.js +10 -0
- package/src/vendors/transactions/Transaction.js +9 -0
- package/src/vendors/transactions/data/internal.js +22 -0
- package/src/vendors/transactions/data/v2.js +21 -0
- package/src/vendors/transactions/resolve.js +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,24 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [1.5.0](https://github.com/ExodusMovement/assets/compare/@exodus/sui-api@1.4.1...@exodus/sui-api@1.5.0) (2026-11-02)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* **sui-api:** spend sui address balances in sends ([#9144](https://github.com/ExodusMovement/assets/issues/9144)) ([d4b001d](https://github.com/ExodusMovement/assets/commit/d4b001d349cd8418e19638c8ff0f7d58c64e3c49))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
## [1.4.1](https://github.com/ExodusMovement/assets/compare/@exodus/sui-api@1.4.0...@exodus/sui-api@1.4.1) (2026-11-02)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Bug Fixes
|
|
19
|
+
|
|
20
|
+
* **sui-api:** count sui address balances in balances ([#9140](https://github.com/ExodusMovement/assets/issues/9140)) ([9486138](https://github.com/ExodusMovement/assets/commit/9486138e6cedc7edf88f136bdfc998d735756ece))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
6
24
|
## [1.4.0](https://github.com/ExodusMovement/assets/compare/@exodus/sui-api@1.3.1...@exodus/sui-api@1.4.0) (2026-10-22)
|
|
7
25
|
|
|
8
26
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/sui-api",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Transaction monitors, RPC with the blockchain node, and other networking code for Sui",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -24,16 +24,16 @@
|
|
|
24
24
|
"@exodus/asset-lib": "^5.0.0",
|
|
25
25
|
"@exodus/basic-utils": "^5.1.0",
|
|
26
26
|
"@exodus/crypto": "^1.0.0-rc.26",
|
|
27
|
-
"@exodus/currency": "^6.0.1",
|
|
28
27
|
"@exodus/models": "^13.0.0",
|
|
29
|
-
"@exodus/sui-lib": "^1.
|
|
28
|
+
"@exodus/sui-lib": "^1.4.0",
|
|
30
29
|
"@exodus/sui-meta": "^1.2.0",
|
|
31
30
|
"lodash": "^4.17.21",
|
|
32
31
|
"minimalistic-assert": "^1.0.1",
|
|
33
32
|
"ms": "^2.1.3"
|
|
34
33
|
},
|
|
35
34
|
"devDependencies": {
|
|
36
|
-
"@exodus/assets-testing": "^1.0.0"
|
|
35
|
+
"@exodus/assets-testing": "^1.0.0",
|
|
36
|
+
"@exodus/currency": "^6.0.1"
|
|
37
37
|
},
|
|
38
38
|
"homepage": "https://github.com/ExodusMovement/assets/tree/master/sui/sui-api",
|
|
39
39
|
"bugs": {
|
|
@@ -43,5 +43,5 @@
|
|
|
43
43
|
"type": "git",
|
|
44
44
|
"url": "git+https://github.com/ExodusMovement/assets.git"
|
|
45
45
|
},
|
|
46
|
-
"gitHead": "
|
|
46
|
+
"gitHead": "05ed20b7d858ef18380e7108022c9d323a34f5f4"
|
|
47
47
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const ZERO = BigInt(0)
|
|
2
|
+
|
|
3
|
+
const min = (a, b) => (a < b ? a : b)
|
|
4
|
+
|
|
5
|
+
const planBaseSpend = ({ amount, coins, addressBalance, gasBudget }) => {
|
|
6
|
+
if (addressBalance === ZERO || coins >= amount + gasBudget) {
|
|
7
|
+
return { fromCoins: amount, fromAddressBalance: ZERO, gasFromAddressBalance: false }
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (addressBalance >= amount + gasBudget) {
|
|
11
|
+
return { fromCoins: ZERO, fromAddressBalance: amount, gasFromAddressBalance: true }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (coins < gasBudget && addressBalance >= gasBudget) {
|
|
15
|
+
const fromAddressBalance = min(addressBalance - gasBudget, amount)
|
|
16
|
+
return {
|
|
17
|
+
fromCoins: amount - fromAddressBalance,
|
|
18
|
+
fromAddressBalance,
|
|
19
|
+
gasFromAddressBalance: true,
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const fromAddressBalance = min(addressBalance, amount)
|
|
24
|
+
return {
|
|
25
|
+
fromCoins: amount - fromAddressBalance,
|
|
26
|
+
fromAddressBalance,
|
|
27
|
+
gasFromAddressBalance: false,
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const planTokenSpend = ({
|
|
32
|
+
amount,
|
|
33
|
+
coins,
|
|
34
|
+
addressBalance,
|
|
35
|
+
gasCoins,
|
|
36
|
+
gasAddressBalance,
|
|
37
|
+
gasBudget,
|
|
38
|
+
}) => {
|
|
39
|
+
const fromAddressBalance = min(addressBalance, amount - min(coins, amount))
|
|
40
|
+
return {
|
|
41
|
+
fromCoins: amount - fromAddressBalance,
|
|
42
|
+
fromAddressBalance,
|
|
43
|
+
gasFromAddressBalance: gasCoins < gasBudget && gasAddressBalance >= gasBudget,
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const planSpend = ({ isBaseAsset, ...balances }) =>
|
|
48
|
+
isBaseAsset ? planBaseSpend(balances) : planTokenSpend(balances)
|
package/src/api.js
CHANGED
|
@@ -181,9 +181,10 @@ export class SuiApi {
|
|
|
181
181
|
variables: { owner: address },
|
|
182
182
|
getConnection: (data) => data.address?.balances,
|
|
183
183
|
})
|
|
184
|
-
return balances.map(({ coinType, totalBalance }) => ({
|
|
184
|
+
return balances.map(({ coinType, totalBalance, addressBalance }) => ({
|
|
185
185
|
coinType: toJsonRpcTypeTag(coinType.repr),
|
|
186
186
|
totalBalance,
|
|
187
|
+
addressBalance,
|
|
187
188
|
}))
|
|
188
189
|
}
|
|
189
190
|
|
|
@@ -214,8 +215,9 @@ export class SuiApi {
|
|
|
214
215
|
const cached = this.#epoch
|
|
215
216
|
if (cached && SynchronizedTime.now() < cached.startMs + cached.durationMs) return cached
|
|
216
217
|
|
|
217
|
-
const { epoch } = await this.#query(LATEST_EPOCH)
|
|
218
|
+
const { chainIdentifier, epoch } = await this.#query(LATEST_EPOCH)
|
|
218
219
|
this.#epoch = {
|
|
220
|
+
chainIdentifier,
|
|
219
221
|
epochId: Number(epoch.epochId),
|
|
220
222
|
referenceGasPrice: String(epoch.referenceGasPrice),
|
|
221
223
|
startMs: Number(epoch.systemState.startMs.json),
|
|
@@ -224,6 +226,20 @@ export class SuiApi {
|
|
|
224
226
|
return this.#epoch
|
|
225
227
|
}
|
|
226
228
|
|
|
229
|
+
async getValidDuringExpiration() {
|
|
230
|
+
const { chainIdentifier, epochId } = await this.#getEpoch()
|
|
231
|
+
return {
|
|
232
|
+
ValidDuring: {
|
|
233
|
+
minEpoch: String(epochId),
|
|
234
|
+
maxEpoch: String(epochId + 1),
|
|
235
|
+
minTimestamp: null,
|
|
236
|
+
maxTimestamp: null,
|
|
237
|
+
chain: chainIdentifier,
|
|
238
|
+
nonce: Math.floor(Math.random() * 2 ** 32),
|
|
239
|
+
},
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
227
243
|
async getReferenceGasPrice() {
|
|
228
244
|
const { referenceGasPrice } = await this.#getEpoch()
|
|
229
245
|
return referenceGasPrice
|
package/src/balances.js
CHANGED
|
@@ -1,29 +1,29 @@
|
|
|
1
1
|
import { getStakingBalances } from './staking/staking-balances.js'
|
|
2
2
|
|
|
3
|
+
const getAddressBalance = ({ asset, accountState }) =>
|
|
4
|
+
asset.currency.baseUnit(accountState.addressBalances[asset.name] ?? 0)
|
|
5
|
+
|
|
3
6
|
export const getBalances = ({ asset, accountState }) => {
|
|
4
7
|
const isBaseAsset = asset.name === asset.baseAssetName
|
|
8
|
+
const addressBalance = getAddressBalance({ asset, accountState })
|
|
5
9
|
|
|
6
10
|
if (isBaseAsset) {
|
|
7
|
-
const
|
|
11
|
+
const coins = accountState.utxos.value
|
|
12
|
+
const spendable = coins.add(addressBalance)
|
|
8
13
|
const stakingBalances = getStakingBalances({ asset, stakingInfo: accountState.stakingInfo })
|
|
9
14
|
const { staking, staked, unstaked } = stakingBalances
|
|
10
15
|
return {
|
|
11
16
|
total: spendable.add(staking).add(staked).add(unstaked),
|
|
12
17
|
spendable,
|
|
13
|
-
stakeable:
|
|
18
|
+
stakeable: coins,
|
|
14
19
|
...stakingBalances,
|
|
15
20
|
}
|
|
16
21
|
}
|
|
17
22
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
total: accountState.tokenUtxos[asset.name].value,
|
|
21
|
-
spendable: accountState.tokenUtxos[asset.name].value,
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
23
|
+
const coins = accountState.tokenUtxos[asset.name]?.value ?? asset.currency.ZERO
|
|
24
|
+
const spendable = coins.add(addressBalance)
|
|
25
25
|
return {
|
|
26
|
-
total:
|
|
27
|
-
spendable
|
|
26
|
+
total: spendable,
|
|
27
|
+
spendable,
|
|
28
28
|
}
|
|
29
29
|
}
|
|
@@ -1,8 +1,41 @@
|
|
|
1
1
|
import { UtxoCollection } from '@exodus/models'
|
|
2
|
+
import { coinTypes, vendorLib } from '@exodus/sui-lib'
|
|
2
3
|
import assert from 'minimalistic-assert'
|
|
3
4
|
|
|
5
|
+
import { planSpend } from './address-balance-spend.js'
|
|
4
6
|
import { Transaction } from './vendors/transactions/index.js'
|
|
5
7
|
|
|
8
|
+
const { normalizeSuiAddress } = vendorLib.utils
|
|
9
|
+
|
|
10
|
+
const ZERO = BigInt(0)
|
|
11
|
+
|
|
12
|
+
const toBaseUnits = (utxos) => BigInt(utxos.value.toBaseString())
|
|
13
|
+
|
|
14
|
+
const toAddressBalance = ({ accountState, assetName }) =>
|
|
15
|
+
BigInt(accountState.addressBalances[assetName] ?? 0)
|
|
16
|
+
|
|
17
|
+
const splitFromCoins = ({ txb, utxos, amount }) => {
|
|
18
|
+
const coinObjectIds = utxos
|
|
19
|
+
.toArray()
|
|
20
|
+
.reverse()
|
|
21
|
+
.map((coin) => coin.coinObjectId)
|
|
22
|
+
|
|
23
|
+
const [inputCoin, ...rest] = coinObjectIds
|
|
24
|
+
if (rest.length > 0) {
|
|
25
|
+
txb.mergeCoins(inputCoin, rest)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const [coin] = txb.splitCoins(inputCoin, [amount])
|
|
29
|
+
return coin
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const redeemFromAddressBalance = ({ txb, amount, type }) =>
|
|
33
|
+
txb.moveCall({
|
|
34
|
+
target: '0x2::coin::redeem_funds',
|
|
35
|
+
typeArguments: [type],
|
|
36
|
+
arguments: [txb.withdrawal({ amount, type })],
|
|
37
|
+
})
|
|
38
|
+
|
|
6
39
|
export const createUnsignedTxFactory = ({ assetClientInterface, serverApi }) => {
|
|
7
40
|
assert(assetClientInterface, 'assetClientInterface is required')
|
|
8
41
|
return async ({
|
|
@@ -19,42 +52,82 @@ export const createUnsignedTxFactory = ({ assetClientInterface, serverApi }) =>
|
|
|
19
52
|
walletAccount,
|
|
20
53
|
})
|
|
21
54
|
|
|
22
|
-
const
|
|
55
|
+
const isBaseAsset = asset.name === asset.baseAssetName
|
|
56
|
+
const tokenUtxos = isBaseAsset
|
|
57
|
+
? null
|
|
58
|
+
: accountState.tokenUtxos[asset.name] ||
|
|
59
|
+
UtxoCollection.createEmpty({ currency: asset.currency })
|
|
60
|
+
|
|
61
|
+
const plan = planSpend({
|
|
62
|
+
isBaseAsset,
|
|
63
|
+
amount: BigInt(amount),
|
|
64
|
+
coins: toBaseUnits(isBaseAsset ? accountState.utxos : tokenUtxos),
|
|
65
|
+
addressBalance: toAddressBalance({ accountState, assetName: asset.name }),
|
|
66
|
+
gasCoins: toBaseUnits(accountState.utxos),
|
|
67
|
+
gasAddressBalance: toAddressBalance({ accountState, assetName: asset.baseAssetName }),
|
|
68
|
+
gasBudget: BigInt(gasBudget),
|
|
69
|
+
})
|
|
23
70
|
|
|
71
|
+
const txb = new Transaction()
|
|
24
72
|
txb.setGasBudget(BigInt(gasBudget))
|
|
25
73
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
74
|
+
const coinType = isBaseAsset ? coinTypes.Base : asset.assetId
|
|
75
|
+
const coinUtxos = isBaseAsset ? accountState.utxos : tokenUtxos
|
|
76
|
+
const splitFromGas = isBaseAsset && !plan.gasFromAddressBalance
|
|
77
|
+
const splitCoinPart = (value) =>
|
|
78
|
+
splitFromGas
|
|
79
|
+
? txb.splitCoins(txb.gas, [value])
|
|
80
|
+
: splitFromCoins({ txb, utxos: coinUtxos, amount: value })
|
|
29
81
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
82
|
+
const parts = []
|
|
83
|
+
if (plan.fromCoins > ZERO) {
|
|
84
|
+
parts.push(splitCoinPart(String(plan.fromCoins)))
|
|
34
85
|
}
|
|
35
86
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
87
|
+
if (plan.fromAddressBalance > ZERO) {
|
|
88
|
+
parts.push(
|
|
89
|
+
redeemFromAddressBalance({
|
|
90
|
+
txb,
|
|
91
|
+
amount: String(plan.fromAddressBalance),
|
|
92
|
+
type: coinType,
|
|
93
|
+
})
|
|
94
|
+
)
|
|
95
|
+
}
|
|
44
96
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
97
|
+
if (parts.length === 0) {
|
|
98
|
+
parts.push(
|
|
99
|
+
splitFromGas || coinUtxos.size > 0
|
|
100
|
+
? splitCoinPart('0')
|
|
101
|
+
: txb.moveCall({ target: '0x2::coin::zero', typeArguments: [coinType] })
|
|
102
|
+
)
|
|
48
103
|
}
|
|
49
104
|
|
|
50
|
-
const [coin] =
|
|
105
|
+
const [coin, ...others] = parts
|
|
106
|
+
if (others.length > 0) {
|
|
107
|
+
txb.mergeCoins(coin, others)
|
|
108
|
+
}
|
|
51
109
|
|
|
52
110
|
txb.transferObjects([coin], txb.pure.address(recipientAddress))
|
|
53
111
|
txb.setSender(senderAddress)
|
|
54
112
|
|
|
113
|
+
const gasFromAddressBalance = plan.gasFromAddressBalance && !buildOptions.onlyTransactionKind
|
|
114
|
+
if (gasFromAddressBalance) {
|
|
115
|
+
txb.setGasPayment([])
|
|
116
|
+
txb.setExpiration(await serverApi.getValidDuringExpiration())
|
|
117
|
+
}
|
|
118
|
+
|
|
55
119
|
return {
|
|
56
120
|
txData: await serverApi.buildTransaction({ transaction: txb, ...buildOptions }),
|
|
57
|
-
txMeta:
|
|
121
|
+
txMeta: {
|
|
122
|
+
addressBalanceWithdrawals:
|
|
123
|
+
plan.fromAddressBalance > ZERO
|
|
124
|
+
? { [asset.name]: String(plan.fromAddressBalance) }
|
|
125
|
+
: Object.create(null),
|
|
126
|
+
gasFromAddressBalance,
|
|
127
|
+
...(normalizeSuiAddress(senderAddress) === normalizeSuiAddress(recipientAddress) && {
|
|
128
|
+
selfSendAmount: String(amount),
|
|
129
|
+
}),
|
|
130
|
+
},
|
|
58
131
|
}
|
|
59
132
|
}
|
|
60
133
|
}
|
package/src/get-fee-async.js
CHANGED
|
@@ -51,7 +51,11 @@ export const getFeeAsyncFactory = ({ assetClientInterface, serverApi, createUnsi
|
|
|
51
51
|
|
|
52
52
|
const gasBudget = getGasBudget({ gasUsed, feeAsset })
|
|
53
53
|
|
|
54
|
-
const
|
|
54
|
+
const coins = baseAssetAccountState.utxos.value
|
|
55
|
+
const addressBalance = feeAsset.currency.baseUnit(
|
|
56
|
+
baseAssetAccountState.addressBalances[feeAsset.name] ?? 0
|
|
57
|
+
)
|
|
58
|
+
const balance = coins.gt(addressBalance) ? coins : addressBalance
|
|
55
59
|
|
|
56
60
|
if (fee.lt(asset.feeAsset.currency.baseUnit(MIN_GAS_BUDGET))) {
|
|
57
61
|
// sometimes we can receive a negative fee because of storage rebate
|
|
@@ -63,6 +67,15 @@ export const getFeeAsyncFactory = ({ assetClientInterface, serverApi, createUnsi
|
|
|
63
67
|
}
|
|
64
68
|
}
|
|
65
69
|
|
|
70
|
+
if (coins.lt(gasBudget) && !addressBalance.lt(gasBudget)) {
|
|
71
|
+
// gas will come from the address balance, which reserves the whole budget,
|
|
72
|
+
// so send max must leave the budget there, not just the fee
|
|
73
|
+
return {
|
|
74
|
+
fee: gasBudget,
|
|
75
|
+
gasBudget: gasBudget.toNumber(),
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
66
79
|
if (gasBudget.gt(balance)) {
|
|
67
80
|
// if the gas budget is greater than balance, allow to try to send with fee
|
|
68
81
|
return {
|
|
@@ -5,7 +5,7 @@ import { getPureBcsSchema } from '../vendors/transactions/serializer.js'
|
|
|
5
5
|
const { fromBase64, normalizeSuiAddress } = vendorLib.utils
|
|
6
6
|
|
|
7
7
|
const SPECIAL_ADDRESS_REGEX = /0x0{63}([\dA-Fa-f])(?![\dA-Fa-f])/g
|
|
8
|
-
const NO_GAS_OBJECT_ID = `0x${'0'.repeat(64)}`
|
|
8
|
+
export const NO_GAS_OBJECT_ID = `0x${'0'.repeat(64)}`
|
|
9
9
|
|
|
10
10
|
export const toJsonRpcTypeTag = (typeTag) =>
|
|
11
11
|
typeTag.replaceAll(SPECIAL_ADDRESS_REGEX, '0x$1').replaceAll(/,\s*/g, ', ')
|
package/src/graphql/queries.js
CHANGED
|
@@ -65,7 +65,7 @@ export const ALL_BALANCES = `
|
|
|
65
65
|
address(address: $owner) {
|
|
66
66
|
balances(first: 50, after: $cursor) {
|
|
67
67
|
pageInfo { hasNextPage endCursor }
|
|
68
|
-
nodes { coinType { repr } totalBalance }
|
|
68
|
+
nodes { coinType { repr } totalBalance addressBalance }
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
}
|
|
@@ -98,6 +98,7 @@ export const MOVE_FUNCTION_PARAMETERS = `
|
|
|
98
98
|
|
|
99
99
|
export const LATEST_EPOCH = `
|
|
100
100
|
query {
|
|
101
|
+
chainIdentifier
|
|
101
102
|
epoch {
|
|
102
103
|
epochId
|
|
103
104
|
referenceGasPrice
|
|
@@ -134,18 +134,18 @@ export const createStakingApi = ({
|
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
const getMaxStakeAmount = ({ asset,
|
|
138
|
-
|
|
137
|
+
const getMaxStakeAmount = ({ asset, stakeable, fee }) =>
|
|
138
|
+
stakeable.sub(fee).sub(estimateFee({ asset })).clampLowerZero()
|
|
139
139
|
|
|
140
140
|
const getLimits = ({ asset, accountState, feeData, operation }) => {
|
|
141
|
-
const {
|
|
141
|
+
const { stakeable, staking, staked, unstaked } = getBalances({ asset, accountState })
|
|
142
142
|
const estimatedFee = estimateFee({ asset })
|
|
143
143
|
|
|
144
144
|
switch (operation) {
|
|
145
145
|
case 'stake':
|
|
146
146
|
return {
|
|
147
147
|
min: getMinStakingAmount(feeData),
|
|
148
|
-
max: getMaxStakeAmount({ asset,
|
|
148
|
+
max: getMaxStakeAmount({ asset, stakeable, fee: estimatedFee }),
|
|
149
149
|
reserve: estimatedFee,
|
|
150
150
|
estimatedFee,
|
|
151
151
|
}
|
|
@@ -165,34 +165,34 @@ export const createStakingApi = ({
|
|
|
165
165
|
const canStake = ({ asset, accountState, feeData, amount, isMax }) => {
|
|
166
166
|
assert(isMax || amount, '`amount` or `isMax` required to stake')
|
|
167
167
|
const minStakingAmount = getMinStakingAmount(feeData)
|
|
168
|
-
const {
|
|
168
|
+
const { stakeable } = getBalances({ asset, accountState })
|
|
169
169
|
const fee = estimateFee({ asset })
|
|
170
|
-
const stakeAmount = isMax ? getMaxStakeAmount({ asset,
|
|
170
|
+
const stakeAmount = isMax ? getMaxStakeAmount({ asset, stakeable, fee }) : amount
|
|
171
171
|
|
|
172
172
|
if (stakeAmount.lt(minStakingAmount)) {
|
|
173
173
|
return fail('BELOW_MINIMUM', minStakingAmount.sub(stakeAmount), asset.name)
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
-
if (
|
|
177
|
-
return fail('INSUFFICIENT_BALANCE', stakeAmount.sub(
|
|
176
|
+
if (stakeable.lt(stakeAmount)) {
|
|
177
|
+
return fail('INSUFFICIENT_BALANCE', stakeAmount.sub(stakeable), asset.name)
|
|
178
178
|
}
|
|
179
179
|
|
|
180
180
|
const required = stakeAmount.add(fee)
|
|
181
|
-
if (
|
|
182
|
-
return fail('INSUFFICIENT_FEE_BALANCE', required.sub(
|
|
181
|
+
if (stakeable.lt(required)) {
|
|
182
|
+
return fail('INSUFFICIENT_FEE_BALANCE', required.sub(stakeable), asset.feeAsset.name)
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
-
return pass(
|
|
185
|
+
return pass(stakeable.sub(required).lt(estimateFee({ asset })) && 'LOW_FEE_BALANCE')
|
|
186
186
|
}
|
|
187
187
|
|
|
188
188
|
const canUnstake = ({ asset, accountState, positionIds }) => {
|
|
189
189
|
const stakes = selectStakes({ stakingInfo: accountState.stakingInfo, positionIds })
|
|
190
190
|
if (stakes.length === 0) return fail('NOTHING_TO_UNSTAKE')
|
|
191
191
|
|
|
192
|
-
const {
|
|
192
|
+
const { stakeable } = getBalances({ asset, accountState })
|
|
193
193
|
const fee = estimateFee({ asset })
|
|
194
|
-
if (
|
|
195
|
-
return fail('INSUFFICIENT_FEE_BALANCE', fee.sub(
|
|
194
|
+
if (stakeable.lt(fee)) {
|
|
195
|
+
return fail('INSUFFICIENT_FEE_BALANCE', fee.sub(stakeable), asset.feeAsset.name)
|
|
196
196
|
}
|
|
197
197
|
|
|
198
198
|
return pass()
|
|
@@ -264,7 +264,7 @@ export const createStakingApi = ({
|
|
|
264
264
|
const prepareStake = async ({ asset, accountState, feeData, address, amount, isMax }) => {
|
|
265
265
|
const minStakingAmount = getMinStakingAmount(feeData)
|
|
266
266
|
const validator = stakingSettings.getValidator()
|
|
267
|
-
const {
|
|
267
|
+
const { stakeable } = getBalances({ asset, accountState })
|
|
268
268
|
const build = (stakeAmount) =>
|
|
269
269
|
buildStakeTransaction({
|
|
270
270
|
sender: address,
|
|
@@ -278,7 +278,7 @@ export const createStakingApi = ({
|
|
|
278
278
|
transaction: build(isMax ? minStakingAmount : amount),
|
|
279
279
|
})
|
|
280
280
|
const stakeAmount = isMax
|
|
281
|
-
? getMaxStakeAmount({ asset,
|
|
281
|
+
? getMaxStakeAmount({ asset, stakeable, fee: estimate.gasBudget })
|
|
282
282
|
: amount
|
|
283
283
|
if (stakeAmount.lt(minStakingAmount)) {
|
|
284
284
|
reject({
|
|
@@ -288,10 +288,10 @@ export const createStakingApi = ({
|
|
|
288
288
|
}
|
|
289
289
|
|
|
290
290
|
const required = stakeAmount.add(estimate.gasBudget)
|
|
291
|
-
if (
|
|
291
|
+
if (stakeable.lt(required)) {
|
|
292
292
|
reject({
|
|
293
293
|
operation: 'stake',
|
|
294
|
-
check: fail('INSUFFICIENT_FEE_BALANCE', required.sub(
|
|
294
|
+
check: fail('INSUFFICIENT_FEE_BALANCE', required.sub(stakeable), asset.feeAsset.name),
|
|
295
295
|
})
|
|
296
296
|
}
|
|
297
297
|
|
|
@@ -310,13 +310,13 @@ export const createStakingApi = ({
|
|
|
310
310
|
const stakedSuiIds = stakes.map(({ id }) => id)
|
|
311
311
|
const build = () => buildUnstakeTransaction({ sender: address, stakedSuiIds })
|
|
312
312
|
const estimate = await simulateGas({ asset, sender: address, transaction: build() })
|
|
313
|
-
const {
|
|
314
|
-
if (
|
|
313
|
+
const { stakeable } = getBalances({ asset, accountState })
|
|
314
|
+
if (stakeable.lt(estimate.gasBudget)) {
|
|
315
315
|
reject({
|
|
316
316
|
operation: 'unstake',
|
|
317
317
|
check: fail(
|
|
318
318
|
'INSUFFICIENT_FEE_BALANCE',
|
|
319
|
-
estimate.gasBudget.sub(
|
|
319
|
+
estimate.gasBudget.sub(stakeable),
|
|
320
320
|
asset.feeAsset.name
|
|
321
321
|
),
|
|
322
322
|
})
|
|
@@ -61,7 +61,7 @@ export class SuiNoHistoryMonitor extends BaseMonitor {
|
|
|
61
61
|
|
|
62
62
|
const ourWalletAddress = await this.aci.getReceiveAddress({ assetName, walletAccount })
|
|
63
63
|
|
|
64
|
-
const { utxos, tokenUtxos, unknownAssetIds } = await this.#getNewAccountState({
|
|
64
|
+
const { utxos, tokenUtxos, addressBalances, unknownAssetIds } = await this.#getNewAccountState({
|
|
65
65
|
address: ourWalletAddress,
|
|
66
66
|
})
|
|
67
67
|
|
|
@@ -81,6 +81,7 @@ export class SuiNoHistoryMonitor extends BaseMonitor {
|
|
|
81
81
|
newData: {
|
|
82
82
|
utxos,
|
|
83
83
|
tokenUtxos,
|
|
84
|
+
addressBalances,
|
|
84
85
|
...stakingData,
|
|
85
86
|
},
|
|
86
87
|
batch,
|
|
@@ -238,9 +239,17 @@ export class SuiNoHistoryMonitor extends BaseMonitor {
|
|
|
238
239
|
}
|
|
239
240
|
|
|
240
241
|
#getNewAccountState = async ({ address }) => {
|
|
241
|
-
const allCoins = await
|
|
242
|
-
owner: address,
|
|
243
|
-
|
|
242
|
+
const [allCoins, allBalances] = await Promise.all([
|
|
243
|
+
this.#serverApi.getAllCoins({ owner: address }),
|
|
244
|
+
this.#serverApi.getAccountAllBalances({ address }),
|
|
245
|
+
])
|
|
246
|
+
|
|
247
|
+
const addressBalanceByCoinType = Object.create(null)
|
|
248
|
+
for (const { coinType, addressBalance } of allBalances) {
|
|
249
|
+
if (addressBalance && addressBalance !== '0') {
|
|
250
|
+
addressBalanceByCoinType[coinType] = addressBalance
|
|
251
|
+
}
|
|
252
|
+
}
|
|
244
253
|
|
|
245
254
|
const groupedCoins = allCoins.reduce((acc, coin) => {
|
|
246
255
|
if (!acc[coin.coinType]) {
|
|
@@ -252,39 +261,43 @@ export class SuiNoHistoryMonitor extends BaseMonitor {
|
|
|
252
261
|
}, Object.create(null))
|
|
253
262
|
|
|
254
263
|
const tokenUtxos = Object.create(null)
|
|
264
|
+
const addressBalances = Object.create(null)
|
|
255
265
|
let baseUtxos = UtxoCollection.createEmpty({
|
|
256
266
|
currency: this.asset.currency,
|
|
257
267
|
})
|
|
258
268
|
|
|
259
269
|
const unknownAssetIds = new Set(
|
|
260
|
-
Object.keys(groupedCoins).
|
|
270
|
+
[...Object.keys(groupedCoins), ...Object.keys(addressBalanceByCoinType)].filter(
|
|
271
|
+
(coinType) => coinType !== coinTypes.Base
|
|
272
|
+
)
|
|
261
273
|
)
|
|
262
274
|
|
|
263
275
|
for (const [assetName, asset] of Object.entries(this.assets)) {
|
|
264
276
|
const isBaseAsset = asset.baseAssetName === asset.name
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
address,
|
|
268
|
-
coinType: coinTypes.Base,
|
|
269
|
-
coins: groupedCoins[coinTypes.Base] || [],
|
|
270
|
-
asset,
|
|
271
|
-
})
|
|
272
|
-
continue
|
|
273
|
-
}
|
|
277
|
+
const coinType = isBaseAsset ? coinTypes.Base : asset.assetId
|
|
278
|
+
if (!isBaseAsset && !unknownAssetIds.delete(coinType)) continue
|
|
274
279
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
tokenUtxos[assetName] = this.#formatUtxos({
|
|
280
|
+
const utxos = this.#formatUtxos({
|
|
278
281
|
address,
|
|
279
|
-
coinType
|
|
280
|
-
coins: groupedCoins[
|
|
282
|
+
coinType,
|
|
283
|
+
coins: groupedCoins[coinType] || [],
|
|
281
284
|
asset,
|
|
282
285
|
})
|
|
286
|
+
if (isBaseAsset) {
|
|
287
|
+
baseUtxos = utxos
|
|
288
|
+
} else {
|
|
289
|
+
tokenUtxos[assetName] = utxos
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (addressBalanceByCoinType[coinType]) {
|
|
293
|
+
addressBalances[assetName] = addressBalanceByCoinType[coinType]
|
|
294
|
+
}
|
|
283
295
|
}
|
|
284
296
|
|
|
285
297
|
return {
|
|
286
298
|
utxos: baseUtxos,
|
|
287
299
|
tokenUtxos,
|
|
300
|
+
addressBalances,
|
|
288
301
|
unknownAssetIds: [...unknownAssetIds],
|
|
289
302
|
}
|
|
290
303
|
}
|
package/src/transform-tx.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { NO_GAS_OBJECT_ID } from './graphql/json-rpc-shapes.js'
|
|
1
2
|
import { parseStakingCommands } from './staking/staking-transactions.js'
|
|
2
3
|
|
|
3
4
|
const parseTransaction = ({ transaction, asset }) => {
|
|
@@ -49,8 +50,10 @@ export const getFeeAmount = ({ gasUsed, feeAsset }) => {
|
|
|
49
50
|
|
|
50
51
|
const parseConfirmations = ({ effects }) => {
|
|
51
52
|
const { status, gasObject } = effects
|
|
53
|
+
const hasGas =
|
|
54
|
+
gasObject?.reference?.version || gasObject?.reference?.objectId === NO_GAS_OBJECT_ID
|
|
52
55
|
|
|
53
|
-
if (!status?.status || !
|
|
56
|
+
if (!status?.status || !hasGas) {
|
|
54
57
|
return {
|
|
55
58
|
confirmations: 0,
|
|
56
59
|
error: 0,
|
package/src/tx-send.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { UtxoCollection } from '@exodus/models'
|
|
1
|
+
import { Address, UtxoCollection } from '@exodus/models'
|
|
2
2
|
import { coinTypes, objectTypes, toCoinType } from '@exodus/sui-lib'
|
|
3
3
|
import assert from 'minimalistic-assert'
|
|
4
4
|
import ms from 'ms'
|
|
@@ -8,6 +8,7 @@ import { applyStakingTx } from './staking/staking-info.js'
|
|
|
8
8
|
import { parseStakingCommands } from './staking/staking-transactions.js'
|
|
9
9
|
import { getFeeAmount } from './transform-tx.js'
|
|
10
10
|
|
|
11
|
+
const ZERO = BigInt(0)
|
|
11
12
|
const LOOKUP_ATTEMPTS = 5
|
|
12
13
|
const LOOKUP_DELAY_MS = ms('3s')
|
|
13
14
|
|
|
@@ -78,12 +79,7 @@ const deriveNewUtxos = ({ mutatedObject, balanceChange, deletedObjects, utxos, a
|
|
|
78
79
|
}
|
|
79
80
|
}
|
|
80
81
|
|
|
81
|
-
const getObjectAndBalanceChangesByType = ({
|
|
82
|
-
objectChanges,
|
|
83
|
-
balanceChanges,
|
|
84
|
-
ownerAddress,
|
|
85
|
-
asset,
|
|
86
|
-
}) => {
|
|
82
|
+
const getObjectAndBalanceChangesByType = ({ objectChanges, balanceChanges, ownerAddress }) => {
|
|
87
83
|
const ownerBalanceChangesByType = balanceChanges.reduce((acc, change) => {
|
|
88
84
|
const { owner, coinType, amount } = change
|
|
89
85
|
if (owner?.AddressOwner !== ownerAddress) {
|
|
@@ -95,8 +91,6 @@ const getObjectAndBalanceChangesByType = ({
|
|
|
95
91
|
return acc
|
|
96
92
|
}, Object.create(null))
|
|
97
93
|
|
|
98
|
-
const wasBaseAssetSent = asset.name === asset.baseAssetName
|
|
99
|
-
|
|
100
94
|
const ownerObjectChangesByType = objectChanges.reduce((acc, change) => {
|
|
101
95
|
const { owner, objectType, type } = change
|
|
102
96
|
|
|
@@ -131,37 +125,110 @@ const getObjectAndBalanceChangesByType = ({
|
|
|
131
125
|
return acc
|
|
132
126
|
}, Object.create(null))
|
|
133
127
|
|
|
134
|
-
|
|
128
|
+
if (!ownerBalanceChangesByType[coinTypes.Base]) {
|
|
129
|
+
throw new Error('In-decidable state change')
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
ownerObjectChangesByType,
|
|
134
|
+
ownerBalanceChangesByType,
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const addCreatedCoin = ({ utxos, created, value, coinType, ownerAddress, digest, asset }) =>
|
|
139
|
+
UtxoCollection.fromArray(
|
|
140
|
+
[
|
|
141
|
+
...utxos.toArray(),
|
|
142
|
+
{
|
|
143
|
+
address: Address.create(ownerAddress, { path: 'm/0/0' }),
|
|
144
|
+
txId: digest,
|
|
145
|
+
digest: created.digest,
|
|
146
|
+
coinObjectId: created.objectId,
|
|
147
|
+
version: created.version,
|
|
148
|
+
coinType,
|
|
149
|
+
confirmations: 1,
|
|
150
|
+
value: asset.currency.baseUnit(String(value)),
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
{ currency: asset.currency }
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
// A self-send's new coin holds the sent amount. Without that amount, a new coin next to
|
|
157
|
+
// no mutated coin holds whatever the coins gained.
|
|
158
|
+
const getCreatedCoinValue = ({ created, mutated, coinChange, selfSendAmount }) => {
|
|
159
|
+
if (created.length !== 1) return ZERO
|
|
160
|
+
if (selfSendAmount !== undefined) return BigInt(selfSendAmount)
|
|
161
|
+
return mutated.length === 0 ? coinChange : ZERO
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// The address balance changes by what the transaction declared: its withdrawals, plus
|
|
165
|
+
// the gas cost when gas is paid from it. Coins take the rest of the owner's net change,
|
|
166
|
+
// split between the mutated coin and the coin a self-send created.
|
|
167
|
+
const applyCoinChanges = ({
|
|
168
|
+
asset,
|
|
169
|
+
coinType,
|
|
170
|
+
objectChanges,
|
|
171
|
+
balanceChange = '0',
|
|
172
|
+
addressBalanceChange,
|
|
173
|
+
selfSendAmount,
|
|
174
|
+
utxos,
|
|
175
|
+
addressBalance = '0',
|
|
176
|
+
ownerAddress,
|
|
177
|
+
digest,
|
|
178
|
+
}) => {
|
|
179
|
+
const { mutated = [], created = [], deleted = [] } = objectChanges ?? Object.create(null)
|
|
180
|
+
const newAddressBalance = BigInt(addressBalance) + addressBalanceChange
|
|
181
|
+
const coinChange = BigInt(balanceChange) - addressBalanceChange
|
|
182
|
+
const createdCoinValue = getCreatedCoinValue({ created, mutated, coinChange, selfSendAmount })
|
|
183
|
+
const mutatedCoinChange = coinChange - createdCoinValue
|
|
135
184
|
|
|
136
185
|
if (
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
186
|
+
newAddressBalance < ZERO ||
|
|
187
|
+
mutated.length > 1 ||
|
|
188
|
+
createdCoinValue < ZERO ||
|
|
189
|
+
(selfSendAmount !== undefined && created.length !== 1) ||
|
|
190
|
+
(mutated.length === 0 && mutatedCoinChange !== ZERO)
|
|
140
191
|
) {
|
|
141
|
-
// expect to have 1 mutated object at point because coins were merged
|
|
142
192
|
throw new Error('In-decidable state change')
|
|
143
193
|
}
|
|
144
194
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
195
|
+
let newUtxos = utxos
|
|
196
|
+
if (mutated.length === 1) {
|
|
197
|
+
newUtxos = deriveNewUtxos({
|
|
198
|
+
mutatedObject: mutated[0],
|
|
199
|
+
balanceChange: String(mutatedCoinChange),
|
|
200
|
+
deletedObjects: deleted,
|
|
201
|
+
utxos,
|
|
202
|
+
asset,
|
|
203
|
+
}).utxos
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (createdCoinValue > ZERO) {
|
|
207
|
+
newUtxos = addCreatedCoin({
|
|
208
|
+
utxos: newUtxos,
|
|
209
|
+
created: created[0],
|
|
210
|
+
value: createdCoinValue,
|
|
211
|
+
coinType,
|
|
212
|
+
ownerAddress,
|
|
213
|
+
digest,
|
|
214
|
+
asset,
|
|
215
|
+
})
|
|
216
|
+
}
|
|
148
217
|
|
|
149
|
-
|
|
218
|
+
return { utxos: newUtxos, addressBalance: String(newAddressBalance) }
|
|
219
|
+
}
|
|
150
220
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
221
|
+
const updateAddressBalances = ({ addressBalances, updates }) => {
|
|
222
|
+
const next = { ...addressBalances }
|
|
223
|
+
for (const [assetName, value] of Object.entries(updates)) {
|
|
224
|
+
if (value === '0') {
|
|
225
|
+
delete next[assetName]
|
|
226
|
+
} else {
|
|
227
|
+
next[assetName] = value
|
|
158
228
|
}
|
|
159
229
|
}
|
|
160
230
|
|
|
161
|
-
return
|
|
162
|
-
ownerObjectChangesByType,
|
|
163
|
-
ownerBalanceChangesByType,
|
|
164
|
-
}
|
|
231
|
+
return next
|
|
165
232
|
}
|
|
166
233
|
|
|
167
234
|
const getRecipientAddress = ({ balanceChanges, ownerAddress }) => {
|
|
@@ -182,7 +249,9 @@ const stringifyFailureReason = (error) => {
|
|
|
182
249
|
}
|
|
183
250
|
|
|
184
251
|
// TODO: extract into separate file
|
|
185
|
-
const getStateChanges = async ({ aci, asset, walletAccount, result }) => {
|
|
252
|
+
const getStateChanges = async ({ aci, asset, walletAccount, result, txMeta }) => {
|
|
253
|
+
const withdrawals = txMeta?.addressBalanceWithdrawals ?? Object.create(null)
|
|
254
|
+
const withdrawn = (assetName) => BigInt(withdrawals[assetName] ?? 0)
|
|
186
255
|
const { feeAsset, currency: assetCurrency, name: assetName } = asset
|
|
187
256
|
const { objectChanges, balanceChanges, digest, effects } = result
|
|
188
257
|
const { gasUsed, status } = effects
|
|
@@ -210,20 +279,33 @@ const getStateChanges = async ({ aci, asset, walletAccount, result }) => {
|
|
|
210
279
|
objectChanges,
|
|
211
280
|
balanceChanges,
|
|
212
281
|
ownerAddress,
|
|
213
|
-
asset,
|
|
214
282
|
})
|
|
215
283
|
|
|
216
284
|
const newAccountState = Object.create(null)
|
|
285
|
+
const addressBalanceUpdates = Object.create(null)
|
|
286
|
+
const baseAssetName = asset.baseAsset.name
|
|
217
287
|
|
|
218
|
-
const
|
|
219
|
-
|
|
288
|
+
const gasCost =
|
|
289
|
+
BigInt(gasUsed.computationCost) + BigInt(gasUsed.storageCost) - BigInt(gasUsed.storageRebate)
|
|
290
|
+
const gasFromAddressBalance = txMeta?.gasFromAddressBalance ? gasCost : ZERO
|
|
291
|
+
const selfSendAmount = txMeta?.selfSendAmount
|
|
292
|
+
|
|
293
|
+
const base = applyCoinChanges({
|
|
294
|
+
asset: asset.baseAsset,
|
|
295
|
+
coinType: coinTypes.Base,
|
|
296
|
+
objectChanges: ownerObjectChangesByType[objectTypes.Base],
|
|
220
297
|
balanceChange: ownerBalanceChangesByType[coinTypes.Base],
|
|
221
|
-
|
|
298
|
+
addressBalanceChange:
|
|
299
|
+
-(wasBaseAssetSent ? withdrawn(baseAssetName) : BigInt(0)) - gasFromAddressBalance,
|
|
300
|
+
selfSendAmount: wasBaseAssetSent ? selfSendAmount : undefined,
|
|
222
301
|
utxos: accountState.utxos,
|
|
223
|
-
|
|
302
|
+
addressBalance: accountState.addressBalances[baseAssetName],
|
|
303
|
+
ownerAddress,
|
|
304
|
+
digest,
|
|
224
305
|
})
|
|
225
306
|
|
|
226
|
-
newAccountState.utxos =
|
|
307
|
+
newAccountState.utxos = base.utxos
|
|
308
|
+
addressBalanceUpdates[baseAssetName] = base.addressBalance
|
|
227
309
|
|
|
228
310
|
const parsedStaking = wasBaseAssetSent
|
|
229
311
|
? parseStakingCommands(result.transaction?.data?.transaction)
|
|
@@ -233,17 +315,24 @@ const getStateChanges = async ({ aci, asset, walletAccount, result }) => {
|
|
|
233
315
|
if (wasBaseAssetSent) {
|
|
234
316
|
amount = asset.currency.baseUnit(ownerBalanceChangesByType[coinTypes.Base]).add(fee)
|
|
235
317
|
} else {
|
|
236
|
-
const
|
|
237
|
-
const newTokenUtxos = deriveNewUtxos({
|
|
238
|
-
mutatedObject: ownerObjectChangesByType[coinType].mutated[0],
|
|
239
|
-
balanceChange: ownerBalanceChangesByType[asset.assetId],
|
|
240
|
-
deletedObjects: ownerObjectChangesByType[coinType].deleted,
|
|
241
|
-
utxos: accountState.tokenUtxos[asset.name],
|
|
318
|
+
const token = applyCoinChanges({
|
|
242
319
|
asset,
|
|
320
|
+
coinType: asset.assetId,
|
|
321
|
+
objectChanges: ownerObjectChangesByType[toCoinType(asset.assetId)],
|
|
322
|
+
balanceChange: ownerBalanceChangesByType[asset.assetId],
|
|
323
|
+
addressBalanceChange: -withdrawn(asset.name),
|
|
324
|
+
selfSendAmount,
|
|
325
|
+
utxos:
|
|
326
|
+
accountState.tokenUtxos[asset.name] ||
|
|
327
|
+
UtxoCollection.createEmpty({ currency: asset.currency }),
|
|
328
|
+
addressBalance: accountState.addressBalances[asset.name],
|
|
329
|
+
ownerAddress,
|
|
330
|
+
digest,
|
|
243
331
|
})
|
|
244
332
|
|
|
245
|
-
newAccountState.tokenUtxos = { [asset.name]:
|
|
246
|
-
|
|
333
|
+
newAccountState.tokenUtxos = { [asset.name]: token.utxos }
|
|
334
|
+
addressBalanceUpdates[asset.name] = token.addressBalance
|
|
335
|
+
amount = asset.currency.baseUnit(ownerBalanceChangesByType[asset.assetId] ?? 0)
|
|
247
336
|
}
|
|
248
337
|
|
|
249
338
|
const staking = parsedStaking && { ...parsedStaking, amount: amount.abs().toBaseString() }
|
|
@@ -275,6 +364,10 @@ const getStateChanges = async ({ aci, asset, walletAccount, result }) => {
|
|
|
275
364
|
}),
|
|
276
365
|
}),
|
|
277
366
|
utxos: newAccountState.utxos,
|
|
367
|
+
addressBalances: updateAddressBalances({
|
|
368
|
+
addressBalances: accountState.addressBalances,
|
|
369
|
+
updates: addressBalanceUpdates,
|
|
370
|
+
}),
|
|
278
371
|
tokenUtxos: {
|
|
279
372
|
...accountState.tokenUtxos,
|
|
280
373
|
...(newAccountState.tokenUtxos && newAccountState.tokenUtxos[asset.name]
|
|
@@ -324,6 +417,7 @@ export const txSendFactory = ({
|
|
|
324
417
|
walletAccount,
|
|
325
418
|
result,
|
|
326
419
|
aci,
|
|
420
|
+
txMeta: unsignedTx.txMeta,
|
|
327
421
|
})
|
|
328
422
|
|
|
329
423
|
let batch = aci.createOperationsBatch()
|
|
@@ -14,6 +14,16 @@ function Pure(data) {
|
|
|
14
14
|
|
|
15
15
|
const Inputs = {
|
|
16
16
|
Pure,
|
|
17
|
+
FundsWithdrawal({ amount, type }) {
|
|
18
|
+
return {
|
|
19
|
+
$kind: 'FundsWithdrawal',
|
|
20
|
+
FundsWithdrawal: {
|
|
21
|
+
reservation: { $kind: 'MaxAmountU64', MaxAmountU64: String(amount) },
|
|
22
|
+
typeArg: { $kind: 'Balance', Balance: type },
|
|
23
|
+
withdrawFrom: { $kind: 'Sender', Sender: true },
|
|
24
|
+
},
|
|
25
|
+
}
|
|
26
|
+
},
|
|
17
27
|
ObjectRef({ objectId, digest, version }) {
|
|
18
28
|
return {
|
|
19
29
|
$kind: 'Object',
|
|
@@ -365,6 +365,15 @@ const _Transaction = class _Transaction {
|
|
|
365
365
|
return { $kind: 'GasCoin', GasCoin: true }
|
|
366
366
|
}
|
|
367
367
|
|
|
368
|
+
/** Withdraws `amount` of coin `type` from the sender address balance (SIP-58). */
|
|
369
|
+
withdrawal({ amount, type = '0x2::sui::SUI' }) {
|
|
370
|
+
return __privateMethod(this, _Transaction_instances, addInput_fn).call(
|
|
371
|
+
this,
|
|
372
|
+
'object',
|
|
373
|
+
Inputs.FundsWithdrawal({ amount, type })
|
|
374
|
+
)
|
|
375
|
+
}
|
|
376
|
+
|
|
368
377
|
/**
|
|
369
378
|
* Add a new object input to the transaction using the fully-resolved object reference.
|
|
370
379
|
* If you only have an object ID, use `builder.object(id)` instead.
|
|
@@ -156,11 +156,24 @@ const ObjectArg = safeEnum({
|
|
|
156
156
|
}),
|
|
157
157
|
Receiving: ObjectRef,
|
|
158
158
|
})
|
|
159
|
+
const FundsWithdrawal = object({
|
|
160
|
+
reservation: safeEnum({
|
|
161
|
+
MaxAmountU64: JsonU64,
|
|
162
|
+
}),
|
|
163
|
+
typeArg: safeEnum({
|
|
164
|
+
Balance: string(),
|
|
165
|
+
}),
|
|
166
|
+
withdrawFrom: safeEnum({
|
|
167
|
+
Sender: literal(true),
|
|
168
|
+
Sponsor: literal(true),
|
|
169
|
+
}),
|
|
170
|
+
})
|
|
159
171
|
const CallArg = safeEnum({
|
|
160
172
|
Object: ObjectArg,
|
|
161
173
|
Pure: object({
|
|
162
174
|
bytes: BCSBytes,
|
|
163
175
|
}),
|
|
176
|
+
FundsWithdrawal,
|
|
164
177
|
UnresolvedPure: object({
|
|
165
178
|
value: unknown(),
|
|
166
179
|
}),
|
|
@@ -176,10 +189,19 @@ const NormalizedCallArg = safeEnum({
|
|
|
176
189
|
Pure: object({
|
|
177
190
|
bytes: BCSBytes,
|
|
178
191
|
}),
|
|
192
|
+
FundsWithdrawal,
|
|
179
193
|
})
|
|
180
194
|
const TransactionExpiration = safeEnum({
|
|
181
195
|
None: literal(true),
|
|
182
196
|
Epoch: JsonU64,
|
|
197
|
+
ValidDuring: object({
|
|
198
|
+
minEpoch: nullable(JsonU64),
|
|
199
|
+
maxEpoch: nullable(JsonU64),
|
|
200
|
+
minTimestamp: nullable(JsonU64),
|
|
201
|
+
maxTimestamp: nullable(JsonU64),
|
|
202
|
+
chain: string(),
|
|
203
|
+
nonce: pipe(number(), integer()),
|
|
204
|
+
}),
|
|
183
205
|
})
|
|
184
206
|
const TransactionData = object({
|
|
185
207
|
version: literal(2),
|
|
@@ -86,11 +86,24 @@ const ObjectArg = enumUnion({
|
|
|
86
86
|
}),
|
|
87
87
|
Receiving: ObjectRef,
|
|
88
88
|
})
|
|
89
|
+
const FundsWithdrawal = object({
|
|
90
|
+
reservation: enumUnion({
|
|
91
|
+
MaxAmountU64: JsonU64,
|
|
92
|
+
}),
|
|
93
|
+
typeArg: enumUnion({
|
|
94
|
+
Balance: string(),
|
|
95
|
+
}),
|
|
96
|
+
withdrawFrom: enumUnion({
|
|
97
|
+
Sender: literal(true),
|
|
98
|
+
Sponsor: literal(true),
|
|
99
|
+
}),
|
|
100
|
+
})
|
|
89
101
|
const CallArg = enumUnion({
|
|
90
102
|
Object: ObjectArg,
|
|
91
103
|
Pure: object({
|
|
92
104
|
bytes: BCSBytes,
|
|
93
105
|
}),
|
|
106
|
+
FundsWithdrawal,
|
|
94
107
|
UnresolvedPure: object({
|
|
95
108
|
value: unknown(),
|
|
96
109
|
}),
|
|
@@ -104,6 +117,14 @@ const CallArg = enumUnion({
|
|
|
104
117
|
const TransactionExpiration = enumUnion({
|
|
105
118
|
None: literal(true),
|
|
106
119
|
Epoch: JsonU64,
|
|
120
|
+
ValidDuring: object({
|
|
121
|
+
minEpoch: nullable(JsonU64),
|
|
122
|
+
maxEpoch: nullable(JsonU64),
|
|
123
|
+
minTimestamp: nullable(JsonU64),
|
|
124
|
+
maxTimestamp: nullable(JsonU64),
|
|
125
|
+
chain: string(),
|
|
126
|
+
nonce: pipe(number(), integer()),
|
|
127
|
+
}),
|
|
107
128
|
})
|
|
108
129
|
const SerializedTransactionDataV2 = object({
|
|
109
130
|
version: literal(2),
|
|
@@ -42,9 +42,9 @@ async function resolveTransactionPlugin(transactionData, options, next) {
|
|
|
42
42
|
|
|
43
43
|
function validate(transactionData) {
|
|
44
44
|
transactionData.inputs.forEach((input, index) => {
|
|
45
|
-
if (input.$kind !== 'Object' && input.$kind !== 'Pure') {
|
|
45
|
+
if (input.$kind !== 'Object' && input.$kind !== 'Pure' && input.$kind !== 'FundsWithdrawal') {
|
|
46
46
|
throw new Error(
|
|
47
|
-
`Input at index ${index} has not been resolved. Expected a Pure or
|
|
47
|
+
`Input at index ${index} has not been resolved. Expected a Pure, Object or FundsWithdrawal input, but found ${JSON.stringify(
|
|
48
48
|
input
|
|
49
49
|
)}`
|
|
50
50
|
)
|