@ocap/tx-protocols 1.18.17 → 1.18.19
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/lib/execute.js +3 -1
- package/lib/index.js +2 -0
- package/lib/pipes/ensure-cost.js +5 -1
- package/lib/protocols/governance/revoke-stake.js +18 -5
- package/lib/protocols/governance/slash-stake.js +249 -0
- package/lib/protocols/governance/stake.js +11 -3
- package/lib/util.js +29 -10
- package/package.json +14 -14
package/lib/execute.js
CHANGED
|
@@ -62,11 +62,13 @@ module.exports = ({ filter, runAsLambda }) => {
|
|
|
62
62
|
// we should only flush events when retry is not supported
|
|
63
63
|
// otherwise the outer caller should handle these 2
|
|
64
64
|
if (shouldPersistTx(err) && isRetrySupported === false) {
|
|
65
|
-
|
|
65
|
+
let txState;
|
|
66
66
|
try {
|
|
67
|
+
txState = context.states.tx.create(context, err ? err.code || 'INTERNAL' : 'OK');
|
|
67
68
|
await context.statedb.tx.create(txState.hash, txState, context);
|
|
68
69
|
} catch (e) {
|
|
69
70
|
console.error('Failed to save transaction to statedb', e);
|
|
71
|
+
return reject(e);
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
flushEvents(context, { txState });
|
package/lib/index.js
CHANGED
|
@@ -17,6 +17,7 @@ const createToken = require('./protocols/token/create');
|
|
|
17
17
|
const stake = require('./protocols/governance/stake');
|
|
18
18
|
const revokeStake = require('./protocols/governance/revoke-stake');
|
|
19
19
|
const claimStake = require('./protocols/governance/claim-stake');
|
|
20
|
+
const slashStake = require('./protocols/governance/slash-stake');
|
|
20
21
|
const depositTokenV2 = require('./protocols/token/deposit-v2');
|
|
21
22
|
const withdrawTokenV2 = require('./protocols/token/withdraw-v2');
|
|
22
23
|
const createRollup = require('./protocols/rollup/create');
|
|
@@ -65,6 +66,7 @@ const createExecutor = ({ filter, runAsLambda }) => {
|
|
|
65
66
|
stake,
|
|
66
67
|
revokeStake,
|
|
67
68
|
claimStake,
|
|
69
|
+
slashStake,
|
|
68
70
|
|
|
69
71
|
// rollup
|
|
70
72
|
createRollup,
|
package/lib/pipes/ensure-cost.js
CHANGED
|
@@ -43,7 +43,11 @@ module.exports = function CreateEnsureTxCostPipe({ attachSenderChanges = true }
|
|
|
43
43
|
const gasStake = {};
|
|
44
44
|
if (token && pk && JWT.verify(token, pk, { enforceTimestamp: true, tolerance: 5 })) {
|
|
45
45
|
const decoded = JWT.decode(token);
|
|
46
|
-
|
|
46
|
+
const txHashes = (decoded.txHash || '')
|
|
47
|
+
.split(',')
|
|
48
|
+
.map((x) => x.trim())
|
|
49
|
+
.filter(Boolean);
|
|
50
|
+
if (txHashes.includes(txHash)) {
|
|
47
51
|
gasStake.owner = toAddress(decoded.iss);
|
|
48
52
|
gasStake.stakeId = toStakeAddress(gasStake.owner, gasStake.owner);
|
|
49
53
|
} else {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
const isEmpty = require('empty-value');
|
|
2
2
|
const { BN } = require('@ocap/util');
|
|
3
|
+
const { Joi, schemas } = require('@arcblock/validator');
|
|
4
|
+
const { CustomError: Error } = require('@ocap/util/lib/error');
|
|
3
5
|
const { Runner, pipes } = require('@ocap/tx-pipeline');
|
|
4
6
|
const { account, stake } = require('@ocap/state');
|
|
5
7
|
const { getRelatedAddresses } = require('@ocap/util/lib/get-related-addr');
|
|
@@ -15,6 +17,17 @@ const runner = new Runner();
|
|
|
15
17
|
|
|
16
18
|
runner.use(pipes.VerifyMultiSig(0));
|
|
17
19
|
|
|
20
|
+
// 0. verify itx
|
|
21
|
+
const schema = Joi.object({
|
|
22
|
+
address: Joi.DID().role('ROLE_STAKE').required(),
|
|
23
|
+
outputsList: schemas.multiInput.min(1).required(),
|
|
24
|
+
data: Joi.any().optional(),
|
|
25
|
+
}).options({ stripUnknown: true, noDefaults: false });
|
|
26
|
+
runner.use(({ itx }, next) => {
|
|
27
|
+
const { error } = schema.validate(itx);
|
|
28
|
+
return next(error ? new Error('INVALID_TX', `Invalid itx: ${error.message}`) : null);
|
|
29
|
+
});
|
|
30
|
+
|
|
18
31
|
// 1. verify itx output
|
|
19
32
|
runner.use(
|
|
20
33
|
pipes.VerifyTxInput({
|
|
@@ -29,11 +42,6 @@ runner.use(
|
|
|
29
42
|
// 2. verify itx address
|
|
30
43
|
runner.use(
|
|
31
44
|
pipes.VerifyInfo([
|
|
32
|
-
{
|
|
33
|
-
error: 'INSUFFICIENT_DATA',
|
|
34
|
-
message: 'Can not revoke stake without stake address',
|
|
35
|
-
fn: ({ itx }) => itx.address,
|
|
36
|
-
},
|
|
37
45
|
{
|
|
38
46
|
error: 'INSUFFICIENT_DATA',
|
|
39
47
|
message: 'Can not revoke stake without any output',
|
|
@@ -64,6 +72,11 @@ runner.use(
|
|
|
64
72
|
message: 'This stake address is not revocable',
|
|
65
73
|
fn: ({ stakeState }) => stakeState.revocable,
|
|
66
74
|
},
|
|
75
|
+
{
|
|
76
|
+
error: 'INVALID_TX',
|
|
77
|
+
message: 'Can not revoke assets that are not locked in the stake',
|
|
78
|
+
fn: ({ assets, stakeState }) => assets.every((x) => stakeState.assets.includes(x)),
|
|
79
|
+
},
|
|
67
80
|
])
|
|
68
81
|
);
|
|
69
82
|
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
const isEmpty = require('empty-value');
|
|
2
|
+
const { CustomError: Error } = require('@ocap/util/lib/error');
|
|
3
|
+
const { BN } = require('@ocap/util');
|
|
4
|
+
const { Joi, schemas } = require('@arcblock/validator');
|
|
5
|
+
const { Runner, pipes } = require('@ocap/tx-pipeline');
|
|
6
|
+
const { account, stake, asset } = require('@ocap/state');
|
|
7
|
+
const { getRelatedAddresses } = require('@ocap/util/lib/get-related-addr');
|
|
8
|
+
|
|
9
|
+
// eslint-disable-next-line global-require
|
|
10
|
+
const debug = require('debug')(`${require('../../../package.json').name}:slash-stake`);
|
|
11
|
+
|
|
12
|
+
const { applyTokenUpdates, applyTokenChange } = require('../../util');
|
|
13
|
+
const EnsureTxGas = require('../../pipes/ensure-gas');
|
|
14
|
+
const EnsureTxCost = require('../../pipes/ensure-cost');
|
|
15
|
+
|
|
16
|
+
const runner = new Runner();
|
|
17
|
+
|
|
18
|
+
runner.use(pipes.VerifyMultiSig(0));
|
|
19
|
+
|
|
20
|
+
// 0. verify itx
|
|
21
|
+
const schema = Joi.object({
|
|
22
|
+
address: Joi.DID().role('ROLE_STAKE').required(),
|
|
23
|
+
message: Joi.string().trim().min(1).max(256).required(),
|
|
24
|
+
outputsList: schemas.multiInput.min(1).required(),
|
|
25
|
+
data: Joi.any().optional(),
|
|
26
|
+
}).options({ stripUnknown: true, noDefaults: false });
|
|
27
|
+
runner.use(({ itx }, next) => {
|
|
28
|
+
const { error } = schema.validate(itx);
|
|
29
|
+
return next(error ? new Error('INVALID_TX', `Invalid itx: ${error.message}`) : null);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// 1. verify itx.outputs
|
|
33
|
+
runner.use(
|
|
34
|
+
pipes.VerifyTxInput({
|
|
35
|
+
fieldKey: 'itx.outputs',
|
|
36
|
+
inputsKey: 'outputs',
|
|
37
|
+
sendersKey: 'receivers',
|
|
38
|
+
tokensKey: 'tokens',
|
|
39
|
+
assetsKey: 'assets',
|
|
40
|
+
})
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
// 2. verify output
|
|
44
|
+
runner.use(
|
|
45
|
+
pipes.VerifyInfo([
|
|
46
|
+
{
|
|
47
|
+
error: 'INSUFFICIENT_DATA',
|
|
48
|
+
message: 'Can not slash stake without any output',
|
|
49
|
+
fn: ({ assets, tokens }) => !(isEmpty(tokens) && isEmpty(assets)),
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
error: 'INVALID_TX',
|
|
53
|
+
message: 'Can only send slashed token/assets to specified vault',
|
|
54
|
+
fn: ({ config, receivers }) => receivers.every((x) => x === config.vaults.slashedStake),
|
|
55
|
+
},
|
|
56
|
+
])
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// 3. verify itx size: set hard limit here because more output leads to longer tx execute time
|
|
60
|
+
runner.use(pipes.VerifyListSize({ listKey: ['outputs', 'receivers', 'tokens', 'assets'] }));
|
|
61
|
+
|
|
62
|
+
// 4. verify sender & receiver
|
|
63
|
+
runner.use(pipes.ExtractState({ from: 'tx.from', to: 'senderState', status: 'INVALID_SENDER_STATE', table: 'account' })); // prettier-ignore
|
|
64
|
+
runner.use(pipes.ExtractState({ from: 'receivers', to: 'receiverStates', status: 'INVALID_RECEIVER_STATE', table: 'account' })); // prettier-ignore
|
|
65
|
+
runner.use(pipes.VerifyAccountMigration({ senderKey: 'senderState' }));
|
|
66
|
+
|
|
67
|
+
// 5. verify stake state
|
|
68
|
+
runner.use(pipes.ExtractState({ from: 'itx.address', to: 'stakeState', status: 'INVALID_STAKE_STATE', table: 'stake' })); // prettier-ignore
|
|
69
|
+
runner.use(
|
|
70
|
+
pipes.VerifyInfo([
|
|
71
|
+
{
|
|
72
|
+
error: 'INVALID_TX',
|
|
73
|
+
message: 'You are not allowed to slash from this stake',
|
|
74
|
+
fn: ({ tx, stakeState }) =>
|
|
75
|
+
(isEmpty(stakeState.slashers) ? [stakeState.receiver] : stakeState.slashers).includes(tx.from),
|
|
76
|
+
},
|
|
77
|
+
])
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
// 6. verify token state and balance from both staked and revoked
|
|
81
|
+
runner.use(pipes.ExtractState({ from: 'tokens', to: 'tokenStates', status: 'INVALID_TOKEN', table: 'token' }));
|
|
82
|
+
runner.use((context, next) => {
|
|
83
|
+
const { outputs, stakeState } = context;
|
|
84
|
+
const tokens = {};
|
|
85
|
+
outputs.forEach(({ tokensList }) => {
|
|
86
|
+
tokensList.forEach(({ address, value }) => {
|
|
87
|
+
if (typeof tokens[address] === 'undefined') {
|
|
88
|
+
tokens[address] = new BN(0);
|
|
89
|
+
}
|
|
90
|
+
tokens[address] = tokens[address].add(new BN(value));
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// merge both staked and revoked as locked
|
|
95
|
+
const lockedState = {
|
|
96
|
+
address: stakeState.address,
|
|
97
|
+
tokens: { ...stakeState.tokens },
|
|
98
|
+
assets: [...stakeState.assets, ...stakeState.revokedAssets],
|
|
99
|
+
};
|
|
100
|
+
Object.keys(stakeState.revokedTokens).forEach((address) => {
|
|
101
|
+
lockedState.tokens[address] = new BN(stakeState.revokedTokens[address])
|
|
102
|
+
.add(new BN(stakeState.tokens[address]))
|
|
103
|
+
.toString(10);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
context.lockedState = lockedState;
|
|
107
|
+
context.tokenCondition = {
|
|
108
|
+
owner: stakeState.address,
|
|
109
|
+
tokens: Object.keys(tokens).map((address) => ({ address, value: tokens[address] })),
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
next();
|
|
113
|
+
});
|
|
114
|
+
runner.use(pipes.VerifyTokenBalance({ ownerKey: 'lockedState', conditionKey: 'tokenCondition' }));
|
|
115
|
+
runner.use(
|
|
116
|
+
pipes.VerifyInfo([
|
|
117
|
+
{
|
|
118
|
+
error: 'INVALID_TX',
|
|
119
|
+
message: 'Can not slash assets that are not locked in the stake',
|
|
120
|
+
fn: ({ assets, lockedState }) => assets.every((x) => lockedState.assets.includes(x)),
|
|
121
|
+
},
|
|
122
|
+
])
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
// 7. verify asset state and ownership
|
|
126
|
+
runner.use(pipes.ExtractState({ from: 'assets', to: 'assetStates', status: 'INVALID_ASSET_STATE', table: 'asset' }));
|
|
127
|
+
runner.use(pipes.VerifyTransferrable({ assets: 'assetStates' }));
|
|
128
|
+
runner.use(pipes.VerifyUpdater({ assetKey: 'assetStates', ownerKey: 'stakeState' }));
|
|
129
|
+
|
|
130
|
+
// ensure tx cost and gas
|
|
131
|
+
runner.use(
|
|
132
|
+
EnsureTxGas((context) => {
|
|
133
|
+
const result = { create: 0, update: 2, payment: 0 };
|
|
134
|
+
if (context.receiverStates) {
|
|
135
|
+
result.update += context.receiverStates.length;
|
|
136
|
+
}
|
|
137
|
+
if (context.assetStates) {
|
|
138
|
+
result.update += context.assetStates.length;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return result;
|
|
142
|
+
})
|
|
143
|
+
);
|
|
144
|
+
runner.use(EnsureTxCost({ attachSenderChanges: true }));
|
|
145
|
+
|
|
146
|
+
// 8. update statedb
|
|
147
|
+
runner.use(
|
|
148
|
+
async (context, next) => {
|
|
149
|
+
const {
|
|
150
|
+
tx,
|
|
151
|
+
itx,
|
|
152
|
+
outputs,
|
|
153
|
+
statedb,
|
|
154
|
+
senderState,
|
|
155
|
+
receiverStates = [],
|
|
156
|
+
assetStates = [],
|
|
157
|
+
stakeState,
|
|
158
|
+
updateVaults,
|
|
159
|
+
senderChange,
|
|
160
|
+
} = context;
|
|
161
|
+
|
|
162
|
+
const receiverUpdates = {};
|
|
163
|
+
const assetUpdates = {};
|
|
164
|
+
const stakeUpdates = {
|
|
165
|
+
tokens: stakeState.tokens || {},
|
|
166
|
+
assets: stakeState.assets || [],
|
|
167
|
+
revokedTokens: stakeState.revokedTokens || {},
|
|
168
|
+
revokedAssets: stakeState.revokedAssets || [],
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
outputs.forEach((x) => {
|
|
172
|
+
const { owner, tokensList, assetsList } = x;
|
|
173
|
+
|
|
174
|
+
// move assets to output
|
|
175
|
+
stakeUpdates.assets = stakeUpdates.assets.filter((a) => assetsList.includes(a) === false);
|
|
176
|
+
stakeUpdates.revokedAssets = stakeUpdates.revokedAssets.filter((a) => assetsList.includes(a) === false);
|
|
177
|
+
|
|
178
|
+
// move tokens and assets from staked or revoked to output
|
|
179
|
+
const [newTokens, newRevoked] = applyTokenUpdates(
|
|
180
|
+
tokensList,
|
|
181
|
+
[{ tokens: stakeUpdates.tokens }, { tokens: stakeUpdates.revokedTokens }],
|
|
182
|
+
'sub'
|
|
183
|
+
);
|
|
184
|
+
stakeUpdates.tokens = newTokens.tokens;
|
|
185
|
+
stakeUpdates.revokedTokens = newRevoked.tokens;
|
|
186
|
+
|
|
187
|
+
// Update receiver balance
|
|
188
|
+
const ownerState = receiverStates.find((s) => getRelatedAddresses(s).includes(owner));
|
|
189
|
+
receiverUpdates[ownerState.address] = applyTokenUpdates(tokensList, ownerState, 'add');
|
|
190
|
+
if (senderChange && ownerState.address === senderChange.address) {
|
|
191
|
+
receiverUpdates[ownerState.address] = applyTokenChange(receiverUpdates[ownerState.address], senderChange);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Update asset owner
|
|
195
|
+
assetsList.forEach((a) => {
|
|
196
|
+
assetUpdates[a] = { owner: ownerState.address };
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const [newSenderState, newReceiverStates, newStakeState, newAssetStates] = await Promise.all([
|
|
201
|
+
// Update sender state
|
|
202
|
+
statedb.account.update(
|
|
203
|
+
senderState.address,
|
|
204
|
+
account.update(
|
|
205
|
+
senderState,
|
|
206
|
+
Object.assign(
|
|
207
|
+
{ nonce: tx.nonce },
|
|
208
|
+
receiverUpdates[senderState.address] || (senderChange ? applyTokenChange(senderState, senderChange) : {})
|
|
209
|
+
),
|
|
210
|
+
context
|
|
211
|
+
),
|
|
212
|
+
context
|
|
213
|
+
),
|
|
214
|
+
|
|
215
|
+
// Update receiver states
|
|
216
|
+
Promise.all(
|
|
217
|
+
receiverStates
|
|
218
|
+
.filter((x) => x.address !== senderState.address)
|
|
219
|
+
.map((x) =>
|
|
220
|
+
statedb.account.update(x.address, account.update(x, receiverUpdates[x.address], context), context)
|
|
221
|
+
)
|
|
222
|
+
),
|
|
223
|
+
|
|
224
|
+
// Update stake state
|
|
225
|
+
statedb.stake.update(stakeState.address, stake.update(stakeState, stakeUpdates, context), context),
|
|
226
|
+
|
|
227
|
+
// Transfer assets to output account
|
|
228
|
+
Promise.all(
|
|
229
|
+
assetStates.map((x) =>
|
|
230
|
+
statedb.asset.update(x.address, asset.update(x, assetUpdates[x.address], context), context)
|
|
231
|
+
)
|
|
232
|
+
),
|
|
233
|
+
|
|
234
|
+
updateVaults(),
|
|
235
|
+
]);
|
|
236
|
+
|
|
237
|
+
context.senderState = newSenderState;
|
|
238
|
+
context.receiverStates = newReceiverStates;
|
|
239
|
+
context.stakeState = newStakeState;
|
|
240
|
+
context.assetStates = newAssetStates;
|
|
241
|
+
|
|
242
|
+
debug('slash-stake', { address: itx.address, stakeUpdates });
|
|
243
|
+
|
|
244
|
+
next();
|
|
245
|
+
},
|
|
246
|
+
{ persistError: true }
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
module.exports = runner;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const pick = require('lodash/pick');
|
|
2
2
|
const { promisify } = require('util');
|
|
3
3
|
const isEmpty = require('empty-value');
|
|
4
|
-
const { Joi } = require('@arcblock/validator');
|
|
4
|
+
const { Joi, schemas } = require('@arcblock/validator');
|
|
5
5
|
const { CustomError: Error } = require('@ocap/util/lib/error');
|
|
6
6
|
const { BN, fromTokenToUnit } = require('@ocap/util');
|
|
7
7
|
const { Runner, pipes } = require('@ocap/tx-pipeline');
|
|
@@ -24,7 +24,9 @@ const runner = new Runner();
|
|
|
24
24
|
const schema = Joi.object({
|
|
25
25
|
address: Joi.DID().role('ROLE_STAKE').required(),
|
|
26
26
|
receiver: Joi.DID().required(),
|
|
27
|
+
slashersList: Joi.array().items(Joi.DID()).default([]),
|
|
27
28
|
locked: Joi.boolean().default(false),
|
|
29
|
+
inputsList: schemas.multiInput.min(1).required(),
|
|
28
30
|
message: Joi.string().trim().min(1).max(256).required(),
|
|
29
31
|
revokeWaitingPeriod: Joi.number().integer().min(0).default(0),
|
|
30
32
|
}).options({ stripUnknown: true, noDefaults: false });
|
|
@@ -50,6 +52,11 @@ runner.use(
|
|
|
50
52
|
message: 'Invalid staking address',
|
|
51
53
|
fn: ({ tx, itx }) => toStakeAddress(tx.from, itx.receiver) === itx.address,
|
|
52
54
|
},
|
|
55
|
+
{
|
|
56
|
+
error: 'INVALID_TX',
|
|
57
|
+
message: 'Tx sender can not be slasher',
|
|
58
|
+
fn: ({ tx, itx }) => itx.slashersList.includes(tx.from) === false,
|
|
59
|
+
},
|
|
53
60
|
{
|
|
54
61
|
error: 'INSUFFICIENT_DATA',
|
|
55
62
|
message: 'Can not stake without any token or asset',
|
|
@@ -104,9 +111,7 @@ runner.use(async (context, next) => {
|
|
|
104
111
|
const { token, transaction } = config;
|
|
105
112
|
const { minStake, maxStake } = transaction.txGas;
|
|
106
113
|
|
|
107
|
-
// FIXME: delegation not supported here
|
|
108
114
|
context.isGasStake = isGasStakeAddress(tx.from, itx.address) && isGasStakeInput(inputs, token.address);
|
|
109
|
-
|
|
110
115
|
if (context.isGasStake) {
|
|
111
116
|
const [tokenInput] = inputs[0].tokensList;
|
|
112
117
|
const actualStake = new BN(tokenInput.value);
|
|
@@ -122,8 +127,10 @@ runner.use(async (context, next) => {
|
|
|
122
127
|
|
|
123
128
|
if (context.isGasStake) {
|
|
124
129
|
context.revokeWaitingPeriod = transaction.txGas.stakeLockPeriod;
|
|
130
|
+
context.slashers = [config.moderator.address];
|
|
125
131
|
} else {
|
|
126
132
|
context.revokeWaitingPeriod = itx.revokeWaitingPeriod;
|
|
133
|
+
context.slashers = itx.slashersList;
|
|
127
134
|
}
|
|
128
135
|
|
|
129
136
|
return next();
|
|
@@ -224,6 +231,7 @@ runner.use(
|
|
|
224
231
|
{
|
|
225
232
|
sender: tx.from,
|
|
226
233
|
revocable: !itx.locked,
|
|
234
|
+
slashers: context.slashers,
|
|
227
235
|
revokeWaitingPeriod: context.revokeWaitingPeriod,
|
|
228
236
|
...pick(itx, ['address', 'receiver', 'message', 'data']),
|
|
229
237
|
...stakeUpdates,
|
package/lib/util.js
CHANGED
|
@@ -52,6 +52,8 @@ const decodeAnySafe = (encoded) => {
|
|
|
52
52
|
}
|
|
53
53
|
};
|
|
54
54
|
|
|
55
|
+
// If we are adding tokens to multiple states, same updates applied to all
|
|
56
|
+
// If we are removing tokens from multiple states, applied sequentially
|
|
55
57
|
const applyTokenUpdates = (tokens, state, operator) => {
|
|
56
58
|
if (['add', 'sub'].includes(operator) === false) {
|
|
57
59
|
throw new Error('UNEXPECTED_OPERATOR', `Invalid operator when applyTokenUpdates: ${operator}`);
|
|
@@ -61,23 +63,40 @@ const applyTokenUpdates = (tokens, state, operator) => {
|
|
|
61
63
|
return {};
|
|
62
64
|
}
|
|
63
65
|
|
|
64
|
-
const
|
|
65
|
-
const newTokens = cloneDeep(oldTokens);
|
|
66
|
+
const states = cloneDeep((Array.isArray(state) ? state : [state]).map((x) => ({ tokens: x.tokens || {} })));
|
|
66
67
|
for (let i = 0; i < tokens.length; i++) {
|
|
67
68
|
const { address, value } = tokens[i];
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
const expectedDelta = new BN(value);
|
|
70
|
+
|
|
71
|
+
let fulfilled = new BN(0);
|
|
72
|
+
for (let j = 0; j < states.length; j++) {
|
|
73
|
+
const origin = states[j].tokens;
|
|
74
|
+
const updated = cloneDeep(origin);
|
|
75
|
+
const balance = new BN(origin[address] || 0);
|
|
76
|
+
|
|
77
|
+
let newBalance;
|
|
78
|
+
if (operator === 'add') {
|
|
79
|
+
fulfilled = fulfilled.add(expectedDelta);
|
|
80
|
+
newBalance = balance.add(expectedDelta);
|
|
81
|
+
} else {
|
|
82
|
+
const unfulfilledDelta = expectedDelta.sub(fulfilled);
|
|
83
|
+
const actualDelta = balance.lt(unfulfilledDelta) ? balance : unfulfilledDelta;
|
|
84
|
+
fulfilled = fulfilled.add(actualDelta);
|
|
85
|
+
newBalance = balance.sub(actualDelta);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
updated[address] = newBalance.toString(10);
|
|
89
|
+
states[j].tokens = updated;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (fulfilled.lt(expectedDelta)) {
|
|
72
93
|
throw new Error('NEGATIVE_TOKEN_BALANCE', `Negative token balance when applyTokenUpdates for ${address}`);
|
|
73
94
|
}
|
|
74
|
-
newTokens[address] = newBalance.toString(10);
|
|
75
95
|
}
|
|
76
96
|
|
|
77
|
-
return
|
|
78
|
-
tokens: newTokens,
|
|
79
|
-
};
|
|
97
|
+
return Array.isArray(state) ? states : states[0];
|
|
80
98
|
};
|
|
99
|
+
|
|
81
100
|
const applyTokenChange = (state, change) => {
|
|
82
101
|
const delta = typeof change.delta === 'string' ? new BN(change.delta) : change.delta;
|
|
83
102
|
if (delta.gt(ZERO)) {
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "1.18.
|
|
6
|
+
"version": "1.18.19",
|
|
7
7
|
"description": "Predefined tx pipeline sets to execute certain type of transactions",
|
|
8
8
|
"main": "lib/index.js",
|
|
9
9
|
"files": [
|
|
@@ -21,18 +21,18 @@
|
|
|
21
21
|
"author": "wangshijun <wangshijun2010@gmail.com> (http://github.com/wangshijun)",
|
|
22
22
|
"license": "MIT",
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@arcblock/did": "1.18.
|
|
25
|
-
"@arcblock/did-util": "1.18.
|
|
26
|
-
"@arcblock/jwt": "1.18.
|
|
27
|
-
"@arcblock/validator": "1.18.
|
|
28
|
-
"@ocap/asset": "1.18.
|
|
29
|
-
"@ocap/mcrypto": "1.18.
|
|
30
|
-
"@ocap/merkle-tree": "1.18.
|
|
31
|
-
"@ocap/message": "1.18.
|
|
32
|
-
"@ocap/state": "1.18.
|
|
33
|
-
"@ocap/tx-pipeline": "1.18.
|
|
34
|
-
"@ocap/util": "1.18.
|
|
35
|
-
"@ocap/wallet": "1.18.
|
|
24
|
+
"@arcblock/did": "1.18.19",
|
|
25
|
+
"@arcblock/did-util": "1.18.19",
|
|
26
|
+
"@arcblock/jwt": "1.18.19",
|
|
27
|
+
"@arcblock/validator": "1.18.19",
|
|
28
|
+
"@ocap/asset": "1.18.19",
|
|
29
|
+
"@ocap/mcrypto": "1.18.19",
|
|
30
|
+
"@ocap/merkle-tree": "1.18.19",
|
|
31
|
+
"@ocap/message": "1.18.19",
|
|
32
|
+
"@ocap/state": "1.18.19",
|
|
33
|
+
"@ocap/tx-pipeline": "1.18.19",
|
|
34
|
+
"@ocap/util": "1.18.19",
|
|
35
|
+
"@ocap/wallet": "1.18.19",
|
|
36
36
|
"debug": "^4.3.4",
|
|
37
37
|
"deep-diff": "^1.0.2",
|
|
38
38
|
"empty-value": "^1.0.1",
|
|
@@ -47,5 +47,5 @@
|
|
|
47
47
|
"jest": "^27.5.1",
|
|
48
48
|
"start-server-and-test": "^1.14.0"
|
|
49
49
|
},
|
|
50
|
-
"gitHead": "
|
|
50
|
+
"gitHead": "d13bcea225809e0c0a926cfc246c7a46bd4185f3"
|
|
51
51
|
}
|