@ocap/tx-protocols 1.13.60 → 1.13.64
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/protocols/rollup/claim-reward.js +65 -57
- package/lib/protocols/rollup/create-block.js +60 -162
- package/lib/protocols/token/deposit-v2.js +47 -48
- package/lib/protocols/token/withdraw-v2.js +114 -23
- package/lib/util.js +202 -30
- package/package.json +12 -12
|
@@ -9,10 +9,10 @@ const { account, stake, evidence, rollupBlock, Joi } = require('@ocap/state');
|
|
|
9
9
|
// eslint-disable-next-line global-require
|
|
10
10
|
const debug = require('debug')(`${require('../../../package.json').name}:claim-block-reward`);
|
|
11
11
|
|
|
12
|
+
const { toStakeAddress } = require('@arcblock/did-util');
|
|
12
13
|
const VerifySigners = require('./pipes/verify-signers');
|
|
13
|
-
const { applyTokenChange,
|
|
14
|
+
const { applyTokenChange, splitTxFee, getBNSum, getRewardLocker, RATE_BASE } = require('../../util');
|
|
14
15
|
|
|
15
|
-
const ZERO = new BN(0);
|
|
16
16
|
const runner = new Runner();
|
|
17
17
|
|
|
18
18
|
// 1. verify itx
|
|
@@ -39,12 +39,21 @@ runner.use((context, next) => {
|
|
|
39
39
|
return next(new Error('INVALID_TX', 'itx.publisher must be same with tx.from'));
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
context.lockerAddress =
|
|
42
|
+
context.lockerAddress = getRewardLocker(itx.rollup);
|
|
43
43
|
|
|
44
44
|
return next();
|
|
45
45
|
});
|
|
46
46
|
|
|
47
|
-
// 2.
|
|
47
|
+
// 2. ensure evidence not exist
|
|
48
|
+
runner.use(pipes.ExtractState({ from: 'itx.evidence.hash', to: 'evidenceSeen', status: 'OK', table: 'evidence' }));
|
|
49
|
+
runner.use(pipes.ExtractState({ from: 'itx.blockHash', to: 'blockClaimed', status: 'OK', table: 'evidence' }));
|
|
50
|
+
runner.use(({ evidenceSeen, blockClaimed }, next) => {
|
|
51
|
+
if (evidenceSeen) return next(new Error('INVALID_TX', 'Claim evidence already seen on this chain'));
|
|
52
|
+
if (blockClaimed) return next(new Error('INVALID_TX', 'Block reward already claimed before this tx'));
|
|
53
|
+
return next();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// 3. verify rollup and block state
|
|
48
57
|
runner.use(pipes.ExtractState({ from: 'itx.rollup', to: 'rollupState', status: 'INVALID_ROLLUP', table: 'rollup' }));
|
|
49
58
|
runner.use(pipes.ExtractState({ from: 'itx.blockHash', to: 'blockState', status: 'INVALID_ROLLUP_BLOCK', table: 'rollupBlock' })); // prettier-ignore
|
|
50
59
|
runner.use((context, next) => {
|
|
@@ -55,24 +64,28 @@ runner.use((context, next) => {
|
|
|
55
64
|
if (blockState.height !== itx.blockHeight) {
|
|
56
65
|
return next(new Error('INVALID_TX', 'Rollup block height does not match'));
|
|
57
66
|
}
|
|
67
|
+
|
|
68
|
+
// If the publisher is not the producer, he should wait at least rollupState.publishWaitingPeriod
|
|
69
|
+
if (blockState.proposer !== itx.publisher) {
|
|
70
|
+
const proposedAt = +new Date(blockState.context.genesisTime);
|
|
71
|
+
const publishedAt = +new Date(context.txTime);
|
|
72
|
+
if (proposedAt + rollupState.publishWaitingPeriod * 1000 > publishedAt) {
|
|
73
|
+
return next(new Error('INVALID_TX', 'Rollup block can only be published by producer during waiting period'));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
context.producerStake = toStakeAddress(blockState.proposer, itx.rollup);
|
|
78
|
+
|
|
58
79
|
return next();
|
|
59
80
|
});
|
|
60
81
|
|
|
61
|
-
//
|
|
82
|
+
// 4. verify tx signers & signatures
|
|
62
83
|
runner.use(VerifySigners({ signersKey: 'tx.signaturesList', allowSender: true }));
|
|
63
84
|
runner.use(pipes.VerifyMultiSigV2({ signersKey: 'signers' }));
|
|
64
85
|
|
|
65
|
-
//
|
|
86
|
+
// 5. verify block reward locker
|
|
66
87
|
runner.use(pipes.ExtractState({ from: 'lockerAddress', to: 'lockerState', status: 'INVALID_LOCKER_STATE', table: 'stake' })); // prettier-ignore
|
|
67
|
-
|
|
68
|
-
// 5. ensure evidence not exist
|
|
69
|
-
runner.use(pipes.ExtractState({ from: 'itx.evidence.hash', to: 'evidenceSeen', status: 'OK', table: 'evidence' }));
|
|
70
|
-
runner.use(pipes.ExtractState({ from: 'itx.blockHash', to: 'blockClaimed', status: 'OK', table: 'evidence' }));
|
|
71
|
-
runner.use(({ evidenceSeen, blockClaimed }, next) => {
|
|
72
|
-
if (evidenceSeen) return next(new Error('INVALID_TX', 'Claim evidence already seen on this chain'));
|
|
73
|
-
if (blockClaimed) return next(new Error('INVALID_TX', 'Block reward already claimed before this tx'));
|
|
74
|
-
return next();
|
|
75
|
-
});
|
|
88
|
+
runner.use(pipes.ExtractState({ from: 'producerStake', to: 'stakeState', status: 'INVALID_STAKE_STATE', table: 'stake' })); // prettier-ignore
|
|
76
89
|
|
|
77
90
|
// 6. verify sender and signer states
|
|
78
91
|
runner.use(pipes.ExtractState({ from: 'tx.from', to: 'senderState', status: 'INVALID_SENDER_STATE', table: 'account' })); // prettier-ignore
|
|
@@ -85,8 +98,7 @@ runner.use(pipes.ExtractState({ from: 'rollupState.tokenAddress', to: 'tokenStat
|
|
|
85
98
|
// 8. split and aggregate block reward for each tx and the block
|
|
86
99
|
runner.use(pipes.ExtractState({ from: 'blockState.txs', to: 'txs', status: 'INVALID_TX', table: 'tx' }));
|
|
87
100
|
runner.use(async (context, next) => {
|
|
88
|
-
const { itx, txs, rollupState, blockState, lockerState, tokenState } = context;
|
|
89
|
-
const { depositFeeRate, withdrawFeeRate, minDepositFee, maxDepositFee, minWithdrawFee, maxWithdrawFee } = rollupState;
|
|
101
|
+
const { itx, txs, rollupState, blockState, lockerState, tokenState, stakeState } = context;
|
|
90
102
|
const { proposerFeeShare, publisherFeeShare } = rollupState;
|
|
91
103
|
|
|
92
104
|
const shares = {
|
|
@@ -95,23 +107,28 @@ runner.use(async (context, next) => {
|
|
|
95
107
|
validator: 10000 - publisherFeeShare - proposerFeeShare,
|
|
96
108
|
};
|
|
97
109
|
|
|
98
|
-
// 1. loop through the tx and split reward
|
|
99
110
|
const changes = { account: [], stake: [] };
|
|
111
|
+
|
|
112
|
+
// 0. handle slash case
|
|
113
|
+
// TODO: the producer stake balance should be enough for slashing
|
|
114
|
+
const shouldSlash = blockState.proposer !== itx.publisher;
|
|
115
|
+
if (shouldSlash) {
|
|
116
|
+
const slashAmount = new BN(rollupState.minStakeAmount).mul(new BN(rollupState.publishSlashRate)).div(RATE_BASE);
|
|
117
|
+
const slashShare = slashAmount.div(new BN(2));
|
|
118
|
+
changes.stake.push({ address: stakeState.address, delta: `-${slashAmount.toString(10)}`, action: 'slash' });
|
|
119
|
+
changes.stake.push({ address: lockerState.address, delta: slashShare.toString(10), action: 'slash' });
|
|
120
|
+
changes.account.push({ address: itx.publisher, delta: slashShare.toString(10), action: 'reward' });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 1. loop through the tx and split reward
|
|
100
124
|
txs.forEach((x) => {
|
|
101
|
-
const
|
|
125
|
+
const { actualFee } = x.tx.itxJson;
|
|
126
|
+
const { publisher, proposer, validator } = splitTxFee({ total: actualFee, shares, stringify: false });
|
|
127
|
+
|
|
128
|
+
changes.stake.push({ address: lockerState.address, delta: `-${actualFee}`, action: 'claim' });
|
|
129
|
+
changes.account.push({ address: itx.publisher, delta: publisher.toString(10), action: 'fee' });
|
|
130
|
+
|
|
102
131
|
if (x.type === 'deposit_token_v2') {
|
|
103
|
-
const { fee, feeShares } = splitBlockReward({
|
|
104
|
-
total: amount,
|
|
105
|
-
feeRate: depositFeeRate,
|
|
106
|
-
maxFee: maxDepositFee,
|
|
107
|
-
minFee: minDepositFee,
|
|
108
|
-
shares,
|
|
109
|
-
stringify: false,
|
|
110
|
-
});
|
|
111
|
-
changes.stake.push({ address: lockerState.address, delta: `-${fee}`, action: 'claim' });
|
|
112
|
-
|
|
113
|
-
const { publisher, proposer, validator } = feeShares;
|
|
114
|
-
changes.account.push({ address: itx.publisher, delta: publisher.toString(10), action: 'fee' });
|
|
115
132
|
changes.account.push({ address: x.tx.itxJson.proposer, delta: proposer.toString(10), action: 'fee' });
|
|
116
133
|
|
|
117
134
|
// block and tx signers share the validator part
|
|
@@ -122,18 +139,6 @@ runner.use(async (context, next) => {
|
|
|
122
139
|
changes.account.push({ address: v, delta: validatorShare.toString(10), action: 'fee' })
|
|
123
140
|
);
|
|
124
141
|
} else if (x.type === 'withdraw_token_v2') {
|
|
125
|
-
const { fee, feeShares } = splitBlockReward({
|
|
126
|
-
total: amount,
|
|
127
|
-
feeRate: withdrawFeeRate,
|
|
128
|
-
maxFee: maxWithdrawFee,
|
|
129
|
-
minFee: minWithdrawFee,
|
|
130
|
-
shares,
|
|
131
|
-
stringify: false,
|
|
132
|
-
});
|
|
133
|
-
changes.stake.push({ address: lockerState.address, delta: `-${fee}`, action: 'claim' });
|
|
134
|
-
|
|
135
|
-
const { publisher, proposer, validator } = feeShares;
|
|
136
|
-
changes.account.push({ address: itx.publisher, delta: publisher.toString(10), action: 'fee' });
|
|
137
142
|
changes.account.push({ address: blockState.proposer, delta: proposer.toString(10), action: 'fee' });
|
|
138
143
|
|
|
139
144
|
// block signers share the validator part
|
|
@@ -157,7 +162,7 @@ runner.use(async (context, next) => {
|
|
|
157
162
|
acc[x] = {
|
|
158
163
|
address: x,
|
|
159
164
|
token: tokenState.address,
|
|
160
|
-
delta: grouped.stake[x].
|
|
165
|
+
delta: getBNSum(...grouped.stake[x].map((c) => c.delta)),
|
|
161
166
|
action: grouped.stake[x][grouped.stake[x].length - 1].action,
|
|
162
167
|
};
|
|
163
168
|
return acc;
|
|
@@ -166,7 +171,7 @@ runner.use(async (context, next) => {
|
|
|
166
171
|
acc[x] = {
|
|
167
172
|
address: x,
|
|
168
173
|
token: tokenState.address,
|
|
169
|
-
delta: grouped.account[x].
|
|
174
|
+
delta: getBNSum(...grouped.account[x].map((c) => c.delta)),
|
|
170
175
|
action: grouped.account[x][grouped.account[x].length - 1].action,
|
|
171
176
|
};
|
|
172
177
|
return acc;
|
|
@@ -179,8 +184,6 @@ runner.use(async (context, next) => {
|
|
|
179
184
|
return next();
|
|
180
185
|
});
|
|
181
186
|
|
|
182
|
-
// TODO: verify locker balance
|
|
183
|
-
|
|
184
187
|
// 9. extract all accounts that will be updated exist
|
|
185
188
|
runner.use((context, next) => {
|
|
186
189
|
const { updates, senderState, signerStates } = context;
|
|
@@ -208,17 +211,23 @@ runner.use((context, next) => {
|
|
|
208
211
|
});
|
|
209
212
|
|
|
210
213
|
// 10. update state
|
|
211
|
-
// FIXME: (future) we have a low probability of encounter QLDB 40 documents in a single transaction limit
|
|
212
214
|
runner.use(
|
|
213
215
|
async (context, next) => {
|
|
214
|
-
const { tx, itx, updates, statedb, senderState, blockState, lockerState, accountStates } = context;
|
|
216
|
+
const { tx, itx, updates, statedb, senderState, blockState, lockerState, stakeState, accountStates } = context;
|
|
215
217
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
218
|
+
const [newStakeStates, newAccountStates, newBlockState] = await Promise.all([
|
|
219
|
+
// update stake states
|
|
220
|
+
Promise.all(
|
|
221
|
+
[lockerState, stakeState].map((x) =>
|
|
222
|
+
updates.stake[x.address]
|
|
223
|
+
? statedb.stake.update(
|
|
224
|
+
x.address,
|
|
225
|
+
stake.update(x, applyTokenChange(x, updates.stake[x.address]), context),
|
|
226
|
+
context
|
|
227
|
+
)
|
|
228
|
+
: x
|
|
229
|
+
)
|
|
230
|
+
),
|
|
222
231
|
|
|
223
232
|
// update accounts(proposer, publisher, validator)
|
|
224
233
|
Promise.all(
|
|
@@ -254,12 +263,11 @@ runner.use(
|
|
|
254
263
|
),
|
|
255
264
|
]);
|
|
256
265
|
|
|
257
|
-
context.
|
|
266
|
+
context.stakeStates = newStakeStates;
|
|
258
267
|
context.accountStates = newAccountStates;
|
|
259
268
|
context.blockState = newBlockState;
|
|
260
269
|
|
|
261
270
|
context.updatedAccounts = cloneDeep([...Object.values(updates.account), ...Object.values(updates.stake)]);
|
|
262
|
-
context.updatedAccounts.forEach((x) => (x.delta = x.delta.toString(10))); // eslint-disable-line
|
|
263
271
|
|
|
264
272
|
debug('claim-block-reward', itx);
|
|
265
273
|
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
/* eslint-disable indent */
|
|
2
2
|
const pick = require('lodash/pick');
|
|
3
|
-
const groupBy = require('lodash/groupBy');
|
|
4
3
|
const Error = require('@ocap/util/lib/error');
|
|
5
4
|
const MerkleTree = require('@ocap/merkle-tree');
|
|
6
|
-
const { BN } = require('@ocap/util');
|
|
7
5
|
const { formatMessage } = require('@ocap/message');
|
|
8
6
|
const { toStakeAddress } = require('@arcblock/did-util');
|
|
9
7
|
const { Runner, pipes } = require('@ocap/tx-pipeline');
|
|
10
|
-
const { account, stake, rollup, rollupBlock,
|
|
8
|
+
const { account, stake, rollup, rollupBlock, tx: Tx, Joi } = require('@ocap/state');
|
|
11
9
|
|
|
12
10
|
// eslint-disable-next-line global-require
|
|
13
11
|
const debug = require('debug')(`${require('../../../package.json').name}:create-rollup-block`);
|
|
@@ -15,7 +13,7 @@ const debug = require('debug')(`${require('../../../package.json').name}:create-
|
|
|
15
13
|
const VerifySigners = require('./pipes/verify-signers');
|
|
16
14
|
const VerifyEvidence = require('./pipes/verify-evidence');
|
|
17
15
|
const VerifyPaused = require('./pipes/verify-paused');
|
|
18
|
-
const {
|
|
16
|
+
const { applyTokenChange, ensureBlockReward, getRewardLocker } = require('../../util');
|
|
19
17
|
|
|
20
18
|
const runner = new Runner();
|
|
21
19
|
|
|
@@ -34,6 +32,7 @@ const schema = Joi.object({
|
|
|
34
32
|
proposer: Joi.DID().wallet('ethereum').required(),
|
|
35
33
|
signaturesList: Joi.multiSigSchema.min(1).required(),
|
|
36
34
|
rollup: Joi.DID().role('ROLE_ROLLUP').required(),
|
|
35
|
+
minReward: Joi.BN().min(0).required(),
|
|
37
36
|
data: Joi.any().optional(),
|
|
38
37
|
}).options({ stripUnknown: true, noDefaults: false });
|
|
39
38
|
|
|
@@ -143,7 +142,6 @@ runner.use((context, next) => {
|
|
|
143
142
|
}
|
|
144
143
|
|
|
145
144
|
// ensure minBlockInterval
|
|
146
|
-
// TODO: (future) 这里需要再讨论下
|
|
147
145
|
const minBlockTime = +new Date(previousBlock.context.genesisTime) + rollupState.minBlockInterval * 1000;
|
|
148
146
|
const newBlockTime = +new Date(txTime);
|
|
149
147
|
if (newBlockTime < minBlockTime) {
|
|
@@ -171,57 +169,42 @@ runner.use(
|
|
|
171
169
|
pipes.ExtractState({ from: 'rollupState.tokenAddress', to: 'tokenState', status: 'INVALID_TOKEN', table: 'token' })
|
|
172
170
|
);
|
|
173
171
|
|
|
174
|
-
// 8. verify
|
|
175
|
-
runner.use(pipes.ExtractState({ from: 'itx.txsList', to: '
|
|
176
|
-
runner.use(({
|
|
177
|
-
|
|
172
|
+
// 8. verify txs
|
|
173
|
+
runner.use(pipes.ExtractState({ from: 'itx.txsList', to: 'txStates', status: 'INVALID_TX', table: 'tx' }));
|
|
174
|
+
runner.use(({ itx, txStates }, next) => {
|
|
175
|
+
// ensure not finalized
|
|
176
|
+
const finalizedTx = txStates.some((x) => x.finalized);
|
|
177
|
+
if (finalizedTx) {
|
|
178
178
|
return next(new Error('INVALID_TX', 'Should not include finalized tx in later block'));
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
-
return next();
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
// 9. verify txs
|
|
185
|
-
runner.use(pipes.ExtractState({ from: 'itx.txsList', to: 'txs', status: 'INVALID_TX', table: 'tx' }));
|
|
186
|
-
runner.use(({ itx, txs, rollupState }, next) => {
|
|
187
181
|
// ensure tx are successfully executed
|
|
188
|
-
const invalidStatusTx =
|
|
182
|
+
const invalidStatusTx = txStates.some((x) => x.code !== 'OK');
|
|
189
183
|
if (invalidStatusTx) {
|
|
190
184
|
return next(new Error('INVALID_TX', 'Rollup block can only include successful executed transactions'));
|
|
191
185
|
}
|
|
192
186
|
|
|
193
187
|
// ensure tx type
|
|
194
|
-
const invalidTypeTx =
|
|
188
|
+
const invalidTypeTx = txStates.some((x) => ['deposit_token_v2', 'withdraw_token_v2'].includes(x.type) === false);
|
|
195
189
|
if (invalidTypeTx) {
|
|
196
190
|
return next(new Error('INVALID_TX', "Rollup block can only include 'withdraw_token' and 'deposit_token' tx"));
|
|
197
191
|
}
|
|
198
192
|
|
|
199
193
|
// ensure rollup belonging
|
|
200
|
-
const invalidRollupTx =
|
|
194
|
+
const invalidRollupTx = txStates.some((x) => x.tx.itxJson.rollup !== itx.rollup);
|
|
201
195
|
if (invalidRollupTx) {
|
|
202
196
|
return next(new Error('INVALID_TX', `Rollup block can only include tx from rollup ${itx.rollup}`));
|
|
203
197
|
}
|
|
204
198
|
|
|
205
199
|
// ensure merkleRoot
|
|
206
|
-
const { withdrawFeeRate, minWithdrawFee, maxWithdrawFee } = rollupState;
|
|
207
200
|
const merkleTree = MerkleTree.getBlockMerkleTree(
|
|
208
|
-
|
|
201
|
+
txStates
|
|
209
202
|
.filter((x) => x.type === 'withdraw_token_v2')
|
|
210
|
-
.map((x) => {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
maxFee: maxWithdrawFee,
|
|
216
|
-
minFee: minWithdrawFee,
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
return {
|
|
220
|
-
hash: x.hash,
|
|
221
|
-
to: x.tx.itxJson.to,
|
|
222
|
-
amount: userAmount,
|
|
223
|
-
};
|
|
224
|
-
})
|
|
203
|
+
.map((x) => ({
|
|
204
|
+
hash: x.hash,
|
|
205
|
+
to: x.tx.itxJson.to,
|
|
206
|
+
amount: x.tx.itxJson.token.value,
|
|
207
|
+
}))
|
|
225
208
|
);
|
|
226
209
|
if (merkleTree.getHexRoot() !== itx.merkleRoot) {
|
|
227
210
|
return next(new Error('INVALID_TX', 'Invalid rollup block merkle root'));
|
|
@@ -230,118 +213,45 @@ runner.use(({ itx, txs, rollupState }, next) => {
|
|
|
230
213
|
return next();
|
|
231
214
|
});
|
|
232
215
|
|
|
233
|
-
//
|
|
216
|
+
// 9. verify staking: used for minting & burning
|
|
234
217
|
runner.use((context, next) => {
|
|
235
|
-
const { itx,
|
|
218
|
+
const { itx, txStates } = context;
|
|
236
219
|
|
|
237
|
-
const depositProposerStakeAddr =
|
|
220
|
+
const depositProposerStakeAddr = txStates
|
|
238
221
|
.filter((x) => x.type === 'deposit_token_v2')
|
|
239
222
|
.map((x) => toStakeAddress(x.tx.itxJson.proposer, itx.rollup));
|
|
240
223
|
|
|
241
|
-
const withdrawLockerStakeAddr =
|
|
224
|
+
const withdrawLockerStakeAddr = txStates
|
|
242
225
|
.filter((x) => x.type === 'withdraw_token_v2')
|
|
243
226
|
.map((x) => toStakeAddress(x.tx.from, itx.rollup));
|
|
244
227
|
|
|
245
|
-
context.stakeAddress = [...depositProposerStakeAddr, ...withdrawLockerStakeAddr];
|
|
246
|
-
context.lockerAddress = getBlockRewardLocker(itx.rollup);
|
|
228
|
+
context.stakeAddress = [...depositProposerStakeAddr, ...withdrawLockerStakeAddr, getRewardLocker(itx.rollup)];
|
|
247
229
|
|
|
248
230
|
return next();
|
|
249
231
|
});
|
|
250
|
-
runner.use(pipes.ExtractState({ from: '
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
)
|
|
232
|
+
runner.use(pipes.ExtractState({ from: 'stakeAddress', to: 'stakeStates', status: 'INVALID_STAKE_STATE', table: 'stake' })); // prettier-ignore
|
|
233
|
+
|
|
234
|
+
// 10. ensure block reward and dynamic tx fees
|
|
235
|
+
runner.use((context, next) => {
|
|
236
|
+
const { itx, txStates, rollupState } = context;
|
|
237
|
+
try {
|
|
238
|
+
const result = ensureBlockReward(rollupState, itx.minReward, txStates);
|
|
239
|
+
context.senders = Object.keys(result.accountUpdates);
|
|
240
|
+
Object.assign(context, result);
|
|
241
|
+
return next();
|
|
242
|
+
} catch (err) {
|
|
243
|
+
return next(err);
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
runner.use(pipes.ExtractState({ from: 'senders', to: 'senderStates', status: 'INVALID_SENDER_STATE', table: 'account' })); // prettier-ignore
|
|
254
247
|
|
|
255
248
|
// 11. update state: mint tokens for deposits, burn tokens for withdraws
|
|
256
249
|
runner.use(
|
|
257
250
|
async (context, next) => {
|
|
258
|
-
const {
|
|
259
|
-
|
|
260
|
-
itx,
|
|
261
|
-
txs,
|
|
262
|
-
statedb,
|
|
263
|
-
rollupState,
|
|
264
|
-
senderState,
|
|
265
|
-
tokenState,
|
|
266
|
-
stakeStates,
|
|
267
|
-
lockerAddress,
|
|
268
|
-
lockerState,
|
|
269
|
-
formattedItx,
|
|
270
|
-
} = context;
|
|
271
|
-
const { depositFeeRate, withdrawFeeRate, minDepositFee, maxDepositFee, minWithdrawFee, maxWithdrawFee } =
|
|
272
|
-
rollupState;
|
|
273
|
-
|
|
274
|
-
const ZERO = new BN(0);
|
|
275
|
-
|
|
276
|
-
context.mintedAmount = new BN(0);
|
|
277
|
-
context.burnedAmount = new BN(0);
|
|
278
|
-
context.rewardAmount = new BN(0);
|
|
279
|
-
context.pendingReward = new BN(0);
|
|
280
|
-
|
|
281
|
-
const tokenAddress = tokenState.address;
|
|
282
|
-
|
|
283
|
-
// 1. loop through the tx and gen all updates, minted, burned
|
|
284
|
-
const updates = { stake: [] };
|
|
285
|
-
txs.forEach((x) => {
|
|
286
|
-
const amount = x.tx.itxJson.token.value;
|
|
287
|
-
if (x.type === 'deposit_token_v2') {
|
|
288
|
-
const { fee: feeAmount } = splitBlockReward({
|
|
289
|
-
total: amount,
|
|
290
|
-
feeRate: depositFeeRate,
|
|
291
|
-
maxFee: maxDepositFee,
|
|
292
|
-
minFee: minDepositFee,
|
|
293
|
-
stringify: false,
|
|
294
|
-
});
|
|
295
|
-
context.rewardAmount = context.rewardAmount.add(feeAmount);
|
|
296
|
-
context.mintedAmount = context.mintedAmount.add(new BN(amount));
|
|
297
|
-
|
|
298
|
-
// mint tokens for deposit proposer
|
|
299
|
-
updates.stake.push({
|
|
300
|
-
address: toStakeAddress(x.tx.itxJson.proposer, itx.rollup),
|
|
301
|
-
delta: amount,
|
|
302
|
-
action: 'mint',
|
|
303
|
-
});
|
|
304
|
-
} else if (x.type === 'withdraw_token_v2') {
|
|
305
|
-
const { total: totalAmount, fee: feeAmount } = splitBlockReward({
|
|
306
|
-
total: amount,
|
|
307
|
-
feeRate: withdrawFeeRate,
|
|
308
|
-
maxFee: maxWithdrawFee,
|
|
309
|
-
minFee: minWithdrawFee,
|
|
310
|
-
stringify: false,
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
// save reward to rollup locker stake
|
|
314
|
-
context.pendingReward = context.pendingReward.add(feeAmount);
|
|
315
|
-
|
|
316
|
-
context.rewardAmount = context.rewardAmount.add(feeAmount);
|
|
317
|
-
context.burnedAmount = context.burnedAmount.add(totalAmount);
|
|
318
|
-
|
|
319
|
-
// burn tokens from user locker stakes
|
|
320
|
-
updates.stake.push({ address: toStakeAddress(x.tx.from, itx.rollup), delta: `-${amount}`, action: 'burn' });
|
|
321
|
-
}
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
// 2. aggregate stake updates
|
|
325
|
-
const grouped = { stake: groupBy(updates.stake, 'address') };
|
|
326
|
-
const stakeUpdates = Object.keys(grouped.stake).reduce((acc, x) => {
|
|
327
|
-
acc[x] = {
|
|
328
|
-
address: x,
|
|
329
|
-
token: tokenAddress,
|
|
330
|
-
delta: grouped.stake[x].reduce((a, u) => a.add(new BN(u.delta)), ZERO),
|
|
331
|
-
action: grouped.stake[x][grouped.stake[x].length - 1].action,
|
|
332
|
-
};
|
|
333
|
-
|
|
334
|
-
return acc;
|
|
335
|
-
}, {});
|
|
336
|
-
|
|
337
|
-
const lockerUpdates = applyTokenUpdates(
|
|
338
|
-
[{ address: tokenAddress, value: context.pendingReward.toString(10) }],
|
|
339
|
-
lockerState || { tokens: {} },
|
|
340
|
-
'add'
|
|
341
|
-
);
|
|
251
|
+
const { tx, itx, statedb, rollupState, senderState, stakeStates, txStates, formattedItx } = context;
|
|
252
|
+
const senderStates = context.senderStates || [];
|
|
342
253
|
|
|
343
|
-
|
|
344
|
-
const [newSenderState, newStakeStates, newRollupState, rollupBlockState, newLockerState] = await Promise.all([
|
|
254
|
+
const [newSenderState, newStakeStates, newSenderStates, newRollupState, rollupBlockState] = await Promise.all([
|
|
345
255
|
// update sender(proposer) account
|
|
346
256
|
statedb.account.update(
|
|
347
257
|
senderState.address,
|
|
@@ -352,16 +262,27 @@ runner.use(
|
|
|
352
262
|
// update stake states
|
|
353
263
|
Promise.all(
|
|
354
264
|
stakeStates.map((x) =>
|
|
355
|
-
stakeUpdates[x.address]
|
|
265
|
+
context.stakeUpdates[x.address]
|
|
356
266
|
? statedb.stake.update(
|
|
357
267
|
x.address,
|
|
358
|
-
stake.update(x, applyTokenChange(x, stakeUpdates[x.address]), context),
|
|
268
|
+
stake.update(x, applyTokenChange(x, context.stakeUpdates[x.address]), context),
|
|
359
269
|
context
|
|
360
270
|
)
|
|
361
271
|
: Promise.resolve(x)
|
|
362
272
|
)
|
|
363
273
|
),
|
|
364
274
|
|
|
275
|
+
// update tx owner states: those may get refund
|
|
276
|
+
Promise.all(
|
|
277
|
+
senderStates.map((x) =>
|
|
278
|
+
statedb.account.update(
|
|
279
|
+
x.address,
|
|
280
|
+
account.update(x, applyTokenChange(x, context.accountUpdates[x.address]), context),
|
|
281
|
+
context
|
|
282
|
+
)
|
|
283
|
+
)
|
|
284
|
+
),
|
|
285
|
+
|
|
365
286
|
// Update rollup state
|
|
366
287
|
statedb.rollup.update(
|
|
367
288
|
rollupState.address,
|
|
@@ -385,40 +306,17 @@ runner.use(
|
|
|
385
306
|
context
|
|
386
307
|
),
|
|
387
308
|
|
|
388
|
-
// Update
|
|
389
|
-
|
|
390
|
-
? statedb.stake.update(lockerState.address, stake.update(lockerState, lockerUpdates, context), context)
|
|
391
|
-
: statedb.stake.create(
|
|
392
|
-
itx.address,
|
|
393
|
-
stake.create(
|
|
394
|
-
{
|
|
395
|
-
address: lockerAddress,
|
|
396
|
-
sender: itx.rollup,
|
|
397
|
-
receiver: itx.rollup,
|
|
398
|
-
revocable: false,
|
|
399
|
-
message: 'pending-block-reward',
|
|
400
|
-
revokeWaitingPeriod: 0,
|
|
401
|
-
...lockerUpdates,
|
|
402
|
-
},
|
|
403
|
-
context
|
|
404
|
-
),
|
|
405
|
-
context
|
|
406
|
-
),
|
|
407
|
-
|
|
408
|
-
// Create evidence
|
|
409
|
-
Promise.all(
|
|
410
|
-
itx.txsList.map((x) =>
|
|
411
|
-
statedb.evidence.create(x, evidence.create({ hash: x, data: 'rollup-block' }, context), context)
|
|
412
|
-
)
|
|
413
|
-
),
|
|
309
|
+
// Update tx states: mark as finalized
|
|
310
|
+
Promise.all(txStates.map((x) => statedb.tx.update(x.hash, Tx.update(x, { finalized: true, tx: x.tx }), context))),
|
|
414
311
|
]);
|
|
415
312
|
|
|
416
313
|
context.senderState = newSenderState;
|
|
417
|
-
context.stakeStates =
|
|
314
|
+
context.stakeStates = newStakeStates;
|
|
315
|
+
context.newSenderStates = newSenderStates;
|
|
418
316
|
context.rollupState = newRollupState;
|
|
419
317
|
context.rollupBlockState = rollupBlockState;
|
|
420
|
-
|
|
421
|
-
context.updatedAccounts
|
|
318
|
+
|
|
319
|
+
context.updatedAccounts = [...Object.values(context.stakeUpdates), ...Object.values(context.accountUpdates)];
|
|
422
320
|
|
|
423
321
|
debug('create-rollup-block', { itx, updatedAccounts: context.updatedAccounts });
|
|
424
322
|
|
|
@@ -11,7 +11,7 @@ const debug = require('debug')(`${require('../../../package.json').name}:deposit
|
|
|
11
11
|
|
|
12
12
|
const VerifySigners = require('../rollup/pipes/verify-signers');
|
|
13
13
|
const VerifyPaused = require('../rollup/pipes/verify-paused');
|
|
14
|
-
const { applyTokenUpdates,
|
|
14
|
+
const { applyTokenUpdates, getTxFee, getBNSum, getRewardLocker } = require('../../util');
|
|
15
15
|
|
|
16
16
|
const schema = Joi.object({
|
|
17
17
|
token: Joi.tokenInputSchema.required(),
|
|
@@ -21,6 +21,7 @@ const schema = Joi.object({
|
|
|
21
21
|
hash: Joi.string().regex(Joi.hashRegexp).required(),
|
|
22
22
|
}).required(),
|
|
23
23
|
rollup: Joi.DID().role('ROLE_ROLLUP').required(),
|
|
24
|
+
actualFee: Joi.BN().min(0).required(),
|
|
24
25
|
data: Joi.any().optional(),
|
|
25
26
|
}).options({ stripUnknown: true, noDefaults: false });
|
|
26
27
|
|
|
@@ -82,21 +83,17 @@ runner.use(VerifySigners({ signersKey: 'tx.signaturesList', allowSender: true })
|
|
|
82
83
|
runner.use(pipes.VerifyMultiSigV2({ signersKey: 'signers' }));
|
|
83
84
|
|
|
84
85
|
// 5. verify token state
|
|
85
|
-
runner.use(
|
|
86
|
-
pipes.ExtractState({ from: 'itx.token.address', to: 'tokenState', status: 'INVALID_TOKEN', table: 'token' })
|
|
87
|
-
);
|
|
86
|
+
runner.use(pipes.ExtractState({ from: 'itx.token.address', to: 'tokenState', status: 'INVALID_TOKEN', table: 'token' })); // prettier-ignore
|
|
88
87
|
|
|
89
88
|
// 6. verify staking: get address, extract state, verify amount
|
|
90
89
|
runner.use((context, next) => {
|
|
91
90
|
const { itx } = context;
|
|
92
91
|
context.stakeAddress = toStakeAddress(itx.proposer, itx.rollup);
|
|
93
|
-
context.lockerAddress =
|
|
92
|
+
context.lockerAddress = getRewardLocker(itx.rollup);
|
|
94
93
|
return next();
|
|
95
94
|
});
|
|
96
95
|
runner.use(pipes.ExtractState({ from: 'lockerAddress', to: 'lockerState', status: 'OK', table: 'stake' }));
|
|
97
|
-
runner.use(
|
|
98
|
-
pipes.ExtractState({ from: 'stakeAddress', to: 'stakeState', status: 'INVALID_STAKE_STATE', table: 'stake' })
|
|
99
|
-
);
|
|
96
|
+
runner.use(pipes.ExtractState({ from: 'stakeAddress', to: 'stakeState', status: 'INVALID_STAKE_STATE', table: 'stake' })); // prettier-ignore
|
|
100
97
|
runner.use((context, next) => {
|
|
101
98
|
const { itx, stakeState, tokenState, rollupState } = context;
|
|
102
99
|
if (stakeState.revocable === true) {
|
|
@@ -136,51 +133,45 @@ runner.use((context, next) => {
|
|
|
136
133
|
return next();
|
|
137
134
|
});
|
|
138
135
|
|
|
136
|
+
// 7. verify actualFee
|
|
137
|
+
runner.use((context, next) => {
|
|
138
|
+
const { itx, rollupState, tokenState } = context;
|
|
139
|
+
const { depositFeeRate, maxDepositFee, minDepositFee } = rollupState;
|
|
140
|
+
|
|
141
|
+
const { reward } = getTxFee({
|
|
142
|
+
amount: itx.token.value,
|
|
143
|
+
feeRate: depositFeeRate,
|
|
144
|
+
maxFee: maxDepositFee,
|
|
145
|
+
minFee: minDepositFee,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
if (new BN(itx.actualFee).lt(new BN(reward))) {
|
|
149
|
+
const expected = fromUnitToToken(reward, tokenState.decimal);
|
|
150
|
+
const actual = fromUnitToToken(itx.actualFee, tokenState.decimal);
|
|
151
|
+
return next(new Error('INVALID_TX', `itx.actualFee too low, expect at least ${expected}, got ${actual}`));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return next();
|
|
155
|
+
});
|
|
156
|
+
|
|
139
157
|
// 7. verify sender and signer states
|
|
140
|
-
runner.use(
|
|
141
|
-
|
|
142
|
-
);
|
|
143
|
-
runner.use(
|
|
144
|
-
pipes.ExtractState({ from: 'signers', to: 'signerStates', status: 'INVALID_SIGNER_STATE', table: 'account' })
|
|
145
|
-
);
|
|
158
|
+
runner.use(pipes.ExtractState({ from: 'tx.from', to: 'senderState', status: 'INVALID_SENDER_STATE', table: 'account' })); // prettier-ignore
|
|
159
|
+
runner.use(pipes.ExtractState({ from: 'signers', to: 'signerStates', status: 'INVALID_SIGNER_STATE', table: 'account' })); // prettier-ignore
|
|
146
160
|
runner.use(pipes.VerifyAccountMigration({ signerKey: 'signerStates', senderKey: 'senderState' }));
|
|
147
161
|
|
|
148
162
|
// 8. update state: the token minting is done when deposit finalized in rollup-block
|
|
149
163
|
runner.use(
|
|
150
164
|
async (context, next) => {
|
|
151
|
-
const { tx, itx, statedb,
|
|
152
|
-
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
maxFee: maxDepositFee,
|
|
160
|
-
minFee: minDepositFee,
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
const updatedAccounts = []; // for generating receipts
|
|
164
|
-
updatedAccounts.push({ address: senderState.address, token: tokenAddress, delta: userAmount, action: 'unlock' });
|
|
165
|
-
// stake for tx proposer is decreased
|
|
166
|
-
updatedAccounts.push({
|
|
167
|
-
address: stakeAddress,
|
|
168
|
-
token: tokenAddress,
|
|
169
|
-
delta: `-${itx.token.value}`,
|
|
170
|
-
action: 'unlock',
|
|
171
|
-
});
|
|
172
|
-
// block reward is locked for later claiming
|
|
173
|
-
updatedAccounts.push({
|
|
174
|
-
address: lockerAddress,
|
|
175
|
-
token: tokenAddress,
|
|
176
|
-
delta: feeAmount,
|
|
177
|
-
action: 'pending',
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
const senderUpdates = applyTokenUpdates([{ address: tokenAddress, value: userAmount }], senderState, 'add');
|
|
181
|
-
const stakeUpdates = applyTokenUpdates([itx.token], stakeState, 'sub');
|
|
165
|
+
const { tx, itx, statedb, senderState, stakeState, stakeAddress, lockerState, lockerAddress } = context;
|
|
166
|
+
|
|
167
|
+
const user = itx.token.value;
|
|
168
|
+
const fee = itx.actualFee;
|
|
169
|
+
const total = getBNSum(user, fee);
|
|
170
|
+
|
|
171
|
+
const stakeUpdates = applyTokenUpdates([{ address: itx.token.address, value: total }], stakeState, 'sub');
|
|
172
|
+
const senderUpdates = applyTokenUpdates([{ address: itx.token.address, value: user }], senderState, 'add');
|
|
182
173
|
const lockerUpdates = applyTokenUpdates(
|
|
183
|
-
[{ address:
|
|
174
|
+
[{ address: itx.token.address, value: fee }],
|
|
184
175
|
lockerState || { tokens: {} },
|
|
185
176
|
'add'
|
|
186
177
|
);
|
|
@@ -228,9 +219,17 @@ runner.use(
|
|
|
228
219
|
context.stakeState = newStakeState;
|
|
229
220
|
context.evidenceState = evidenceState;
|
|
230
221
|
context.stakeStates = [newStakeState, newLockerState];
|
|
231
|
-
context.updatedAccounts = updatedAccounts;
|
|
232
222
|
|
|
233
|
-
|
|
223
|
+
context.updatedAccounts = [
|
|
224
|
+
// stake for tx proposer is decreased
|
|
225
|
+
{ address: stakeAddress, token: itx.token.address, delta: `-${total}`, action: 'unlock' },
|
|
226
|
+
// mint to depositor from stake
|
|
227
|
+
{ address: senderState.address, token: itx.token.address, delta: user, action: 'unlock' },
|
|
228
|
+
// tx fee is locked for later claiming
|
|
229
|
+
{ address: lockerAddress, token: itx.token.address, delta: fee, action: 'pending' },
|
|
230
|
+
];
|
|
231
|
+
|
|
232
|
+
debug('deposit-token-v2', itx);
|
|
234
233
|
|
|
235
234
|
next();
|
|
236
235
|
},
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/* eslint-disable indent */
|
|
2
2
|
const Error = require('@ocap/util/lib/error');
|
|
3
|
+
const getListField = require('@ocap/util/lib/get-list-field');
|
|
3
4
|
const { BN, fromUnitToToken } = require('@ocap/util');
|
|
4
5
|
const { Runner, pipes } = require('@ocap/tx-pipeline');
|
|
5
6
|
const { account, stake, Joi } = require('@ocap/state');
|
|
@@ -9,19 +10,22 @@ const { toStakeAddress } = require('@arcblock/did-util');
|
|
|
9
10
|
const debug = require('debug')(`${require('../../../package.json').name}:withdraw-token`);
|
|
10
11
|
|
|
11
12
|
const VerifyPaused = require('../rollup/pipes/verify-paused');
|
|
12
|
-
const { applyTokenUpdates } = require('../../util');
|
|
13
|
+
const { applyTokenUpdates, getTxFee, getBNSum, getRewardLocker } = require('../../util');
|
|
14
|
+
|
|
15
|
+
const verifyMultiSigV2 = pipes.VerifyMultiSigV2({ signersKey: 'signers' });
|
|
13
16
|
|
|
14
17
|
const schema = Joi.object({
|
|
15
18
|
token: Joi.tokenInputSchema.required(),
|
|
16
19
|
to: Joi.DID().wallet('ethereum').required(),
|
|
17
20
|
rollup: Joi.DID().role('ROLE_ROLLUP').required(),
|
|
21
|
+
proposer: Joi.DID().wallet('ethereum').optional().allow('').default(''),
|
|
22
|
+
actualFee: Joi.BN().min(0).required(),
|
|
23
|
+
maxFee: Joi.BN().min(0).required(),
|
|
18
24
|
data: Joi.any().optional(),
|
|
19
25
|
}).options({ stripUnknown: true, noDefaults: false });
|
|
20
26
|
|
|
21
27
|
const runner = new Runner();
|
|
22
28
|
|
|
23
|
-
runner.use(pipes.VerifyMultiSig(0));
|
|
24
|
-
|
|
25
29
|
// 1. verify itx
|
|
26
30
|
runner.use(({ tx, itx }, next) => {
|
|
27
31
|
const { error } = schema.validate(itx);
|
|
@@ -48,18 +52,14 @@ runner.use((context, next) => {
|
|
|
48
52
|
return next();
|
|
49
53
|
});
|
|
50
54
|
|
|
51
|
-
//
|
|
52
|
-
runner.use(
|
|
53
|
-
pipes.ExtractState({ from: 'itx.token.address', to: 'tokenState', status: 'INVALID_TOKEN', table: 'token' })
|
|
54
|
-
);
|
|
55
|
+
// 3. verify token state
|
|
56
|
+
runner.use(pipes.ExtractState({ from: 'itx.token.address', to: 'tokenState', status: 'INVALID_TOKEN', table: 'token' })); // prettier-ignore
|
|
55
57
|
|
|
56
|
-
//
|
|
57
|
-
runner.use(
|
|
58
|
-
pipes.ExtractState({ from: 'tx.from', to: 'senderState', status: 'INVALID_SENDER_STATE', table: 'account' })
|
|
59
|
-
);
|
|
58
|
+
// 4. verify sender and signer states
|
|
59
|
+
runner.use(pipes.ExtractState({ from: 'tx.from', to: 'senderState', status: 'INVALID_SENDER_STATE', table: 'account' })); // prettier-ignore
|
|
60
60
|
runner.use(pipes.VerifyAccountMigration({ senderKey: 'senderState' }));
|
|
61
61
|
|
|
62
|
-
//
|
|
62
|
+
// 5. verify amount
|
|
63
63
|
runner.use((context, next) => {
|
|
64
64
|
const { tx, itx, tokenState, rollupState } = context;
|
|
65
65
|
|
|
@@ -85,18 +85,71 @@ runner.use((context, next) => {
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
// user balance
|
|
88
|
-
|
|
88
|
+
const maxAmount = getBNSum(itx.token.value, itx.actualFee, itx.maxFee);
|
|
89
|
+
context.tokenConditions = { owner: tx.from, tokens: [{ address: itx.token.address, value: maxAmount }] };
|
|
89
90
|
|
|
90
91
|
return next();
|
|
91
92
|
});
|
|
92
93
|
runner.use(pipes.VerifyTokenBalance({ ownerKey: 'senderState', conditionKey: 'tokenConditions' }));
|
|
93
94
|
|
|
95
|
+
// 6. verify actualFee
|
|
96
|
+
runner.use((context, next) => {
|
|
97
|
+
const { itx, rollupState, tokenState } = context;
|
|
98
|
+
const { withdrawFeeRate, maxWithdrawFee, minWithdrawFee } = rollupState;
|
|
99
|
+
|
|
100
|
+
const isFixedFee = new BN(itx.maxFee).isZero();
|
|
101
|
+
if (isFixedFee) {
|
|
102
|
+
const { reward } = getTxFee({
|
|
103
|
+
amount: itx.token.value,
|
|
104
|
+
feeRate: withdrawFeeRate,
|
|
105
|
+
maxFee: maxWithdrawFee,
|
|
106
|
+
minFee: minWithdrawFee,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
if (new BN(itx.actualFee).lt(new BN(reward))) {
|
|
110
|
+
const expected = fromUnitToToken(reward, tokenState.decimal);
|
|
111
|
+
const actual = fromUnitToToken(itx.actualFee, tokenState.decimal);
|
|
112
|
+
return next(new Error('INVALID_TX', `itx.actualFee too low, expect at least ${expected}, got ${actual}`));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return next();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// 7. verify maxFee and conditional multi-sig
|
|
120
|
+
// eslint-disable-next-line consistent-return
|
|
121
|
+
runner.use((context, next) => {
|
|
122
|
+
const { tx, itx } = context;
|
|
123
|
+
const isFixedFee = new BN(itx.maxFee).isZero();
|
|
124
|
+
if (isFixedFee) {
|
|
125
|
+
return next();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const signatures = getListField(tx, 'signatures');
|
|
129
|
+
|
|
130
|
+
// ensure signature count
|
|
131
|
+
if (signatures.length !== 1) {
|
|
132
|
+
return next(new Error('INVALID_TX', 'Withdraw with maxFee should have one multi-sig'));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ensure proposer in signer list
|
|
136
|
+
if (signatures.some((x) => x.signer === itx.proposer) === false) {
|
|
137
|
+
return next(new Error('INVALID_TX', 'itx.proposer must exist in tx.signatures'));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ensure signature is valid
|
|
141
|
+
context.signers = signatures.map((x) => x.signer);
|
|
142
|
+
verifyMultiSigV2(context, next);
|
|
143
|
+
});
|
|
144
|
+
|
|
94
145
|
// 8. verify staking: user withdraw is locked in stake
|
|
95
146
|
runner.use((context, next) => {
|
|
96
147
|
const { itx, senderState } = context;
|
|
97
148
|
context.stakeAddress = toStakeAddress(senderState.address, itx.rollup);
|
|
149
|
+
context.lockerAddress = getRewardLocker(itx.rollup);
|
|
98
150
|
return next();
|
|
99
151
|
});
|
|
152
|
+
runner.use(pipes.ExtractState({ from: 'lockerAddress', to: 'lockerState', status: 'OK', table: 'stake' }));
|
|
100
153
|
runner.use(pipes.ExtractState({ from: 'stakeAddress', to: 'stakeState', status: 'OK', table: 'stake' }));
|
|
101
154
|
runner.use((context, next) => {
|
|
102
155
|
const { stakeState } = context;
|
|
@@ -110,17 +163,28 @@ runner.use((context, next) => {
|
|
|
110
163
|
// 8. update state: the fee splitting and token burning is done when withdraw finalized in rollup-block
|
|
111
164
|
runner.use(
|
|
112
165
|
async (context, next) => {
|
|
113
|
-
const { tx, itx, statedb, stakeState, senderState, stakeAddress } = context;
|
|
114
|
-
|
|
115
|
-
const
|
|
166
|
+
const { tx, itx, statedb, stakeState, senderState, lockerState, lockerAddress, stakeAddress } = context;
|
|
167
|
+
|
|
168
|
+
const total = getBNSum(itx.token.value, itx.actualFee, itx.maxFee);
|
|
169
|
+
const fee = getBNSum(itx.actualFee, itx.maxFee);
|
|
170
|
+
|
|
171
|
+
const senderUpdates = applyTokenUpdates([{ address: itx.token.address, value: total }], senderState, 'sub');
|
|
116
172
|
|
|
117
|
-
|
|
118
|
-
const stakeUpdates = applyTokenUpdates(
|
|
173
|
+
// Burned amount should equal to user received amount
|
|
174
|
+
const stakeUpdates = applyTokenUpdates(
|
|
175
|
+
[{ address: itx.token.address, value: itx.token.value }],
|
|
176
|
+
stakeState || {},
|
|
177
|
+
'add'
|
|
178
|
+
);
|
|
119
179
|
|
|
120
|
-
|
|
121
|
-
|
|
180
|
+
// Fees are locked to reward locker, and funded later
|
|
181
|
+
const lockerUpdates = applyTokenUpdates(
|
|
182
|
+
[{ address: itx.token.address, value: fee }],
|
|
183
|
+
lockerState || { tokens: {} },
|
|
184
|
+
'add'
|
|
185
|
+
);
|
|
122
186
|
|
|
123
|
-
const [newSenderState, newStakeState] = await Promise.all([
|
|
187
|
+
const [newSenderState, newStakeState, newLockerState] = await Promise.all([
|
|
124
188
|
// update user account
|
|
125
189
|
statedb.account.update(
|
|
126
190
|
senderState.address,
|
|
@@ -147,13 +211,40 @@ runner.use(
|
|
|
147
211
|
),
|
|
148
212
|
context
|
|
149
213
|
),
|
|
214
|
+
|
|
215
|
+
// Update pending fee locker state
|
|
216
|
+
lockerState
|
|
217
|
+
? statedb.stake.update(lockerState.address, stake.update(lockerState, lockerUpdates, context), context)
|
|
218
|
+
: statedb.stake.create(
|
|
219
|
+
lockerAddress,
|
|
220
|
+
stake.create(
|
|
221
|
+
{
|
|
222
|
+
address: lockerAddress,
|
|
223
|
+
sender: itx.rollup,
|
|
224
|
+
receiver: itx.rollup,
|
|
225
|
+
revocable: false,
|
|
226
|
+
message: 'pending-block-reward',
|
|
227
|
+
revokeWaitingPeriod: 0,
|
|
228
|
+
...lockerUpdates,
|
|
229
|
+
},
|
|
230
|
+
context
|
|
231
|
+
),
|
|
232
|
+
context
|
|
233
|
+
),
|
|
150
234
|
]);
|
|
151
235
|
|
|
152
236
|
context.senderState = newSenderState;
|
|
153
237
|
context.stakeState = newStakeState;
|
|
154
|
-
context.
|
|
238
|
+
context.lockerState = newLockerState;
|
|
239
|
+
context.stakeStates = [newStakeState, newLockerState];
|
|
240
|
+
|
|
241
|
+
context.updatedAccounts = [
|
|
242
|
+
{ address: senderState.address, token: itx.token.address, delta: `-${total}`, action: 'lock' },
|
|
243
|
+
{ address: stakeAddress, token: itx.token.address, delta: itx.token.value, action: 'lock' },
|
|
244
|
+
{ address: lockerAddress, token: itx.token.address, delta: fee, action: 'pending' },
|
|
245
|
+
];
|
|
155
246
|
|
|
156
|
-
debug('withdraw-token-v2',
|
|
247
|
+
debug('withdraw-token-v2', itx);
|
|
157
248
|
|
|
158
249
|
next();
|
|
159
250
|
},
|
package/lib/util.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
const groupBy = require('lodash/groupBy');
|
|
2
|
+
const flattenDeep = require('lodash/flattenDeep');
|
|
3
|
+
const Error = require('@ocap/util/lib/error');
|
|
1
4
|
const { BN } = require('@ocap/util');
|
|
2
5
|
const { decodeAny } = require('@ocap/message');
|
|
3
6
|
const { toStakeAddress } = require('@arcblock/did-util');
|
|
4
7
|
const cloneDeep = require('lodash/cloneDeep');
|
|
5
8
|
|
|
6
9
|
const ZERO = new BN(0);
|
|
10
|
+
const RATE_BASE = new BN(10000);
|
|
7
11
|
|
|
8
12
|
const decodeAnyNested = (encoded) => {
|
|
9
13
|
if (!encoded) {
|
|
@@ -50,7 +54,7 @@ const decodeAnySafe = (encoded) => {
|
|
|
50
54
|
|
|
51
55
|
const applyTokenUpdates = (tokens, state, operator) => {
|
|
52
56
|
if (['add', 'sub'].includes(operator) === false) {
|
|
53
|
-
throw new Error(`Invalid operator when applyTokenUpdates: ${operator}`);
|
|
57
|
+
throw new Error('FORBIDDEN', `Invalid operator when applyTokenUpdates: ${operator}`);
|
|
54
58
|
}
|
|
55
59
|
|
|
56
60
|
if (!state) {
|
|
@@ -65,7 +69,7 @@ const applyTokenUpdates = (tokens, state, operator) => {
|
|
|
65
69
|
const balance = new BN(oldTokens[address] || 0);
|
|
66
70
|
const newBalance = balance[operator](requirement);
|
|
67
71
|
if (newBalance.lt(ZERO)) {
|
|
68
|
-
throw new Error(`Negative token balance when applyTokenUpdates for ${address}`);
|
|
72
|
+
throw new Error('FORBIDDEN', `Negative token balance when applyTokenUpdates for ${address}`);
|
|
69
73
|
}
|
|
70
74
|
newTokens[address] = newBalance.toString(10);
|
|
71
75
|
}
|
|
@@ -75,10 +79,11 @@ const applyTokenUpdates = (tokens, state, operator) => {
|
|
|
75
79
|
};
|
|
76
80
|
};
|
|
77
81
|
const applyTokenChange = (state, change) => {
|
|
78
|
-
|
|
79
|
-
|
|
82
|
+
const delta = typeof change.delta === 'string' ? new BN(change.delta) : change.delta;
|
|
83
|
+
if (delta.gt(ZERO)) {
|
|
84
|
+
return applyTokenUpdates([{ address: change.token, value: delta.toString(10) }], state, 'add');
|
|
80
85
|
}
|
|
81
|
-
return applyTokenUpdates([{ address: change.token, value:
|
|
86
|
+
return applyTokenUpdates([{ address: change.token, value: delta.abs().toString(10) }], state, 'sub');
|
|
82
87
|
};
|
|
83
88
|
|
|
84
89
|
const fixTokenInput = (input, config) => {
|
|
@@ -91,54 +96,221 @@ const fixTokenInput = (input, config) => {
|
|
|
91
96
|
return input;
|
|
92
97
|
};
|
|
93
98
|
|
|
94
|
-
const
|
|
95
|
-
const
|
|
99
|
+
const getTxFee = ({ amount, feeRate, maxFee, minFee, stringify = true }) => {
|
|
100
|
+
const userAmount = new BN(amount);
|
|
96
101
|
const maxFeeAmount = new BN(maxFee);
|
|
97
102
|
const minFeeAmount = new BN(minFee);
|
|
98
|
-
|
|
103
|
+
|
|
104
|
+
if (feeRate < 0) {
|
|
105
|
+
throw new Error('FORBIDDEN', 'Unexpected negative feeRate when getTxFee, abort!');
|
|
106
|
+
}
|
|
107
|
+
if (userAmount.lt(ZERO)) {
|
|
108
|
+
throw new Error('FORBIDDEN', 'Unexpected negative amount when getTxFee, abort!');
|
|
109
|
+
}
|
|
110
|
+
if (maxFeeAmount.lt(ZERO)) {
|
|
111
|
+
throw new Error('FORBIDDEN', 'Unexpected negative maxFee when getTxFee, abort!');
|
|
112
|
+
}
|
|
113
|
+
if (minFeeAmount.lt(ZERO)) {
|
|
114
|
+
throw new Error('FORBIDDEN', 'Unexpected negative minFee when getTxFee, abort!');
|
|
115
|
+
}
|
|
99
116
|
|
|
100
117
|
// total fee
|
|
101
|
-
let
|
|
102
|
-
if (
|
|
103
|
-
|
|
118
|
+
let rewardAmount = userAmount.mul(new BN(feeRate)).div(RATE_BASE);
|
|
119
|
+
if (rewardAmount.lt(minFeeAmount)) {
|
|
120
|
+
rewardAmount = minFeeAmount;
|
|
104
121
|
}
|
|
105
|
-
if (
|
|
106
|
-
|
|
122
|
+
if (rewardAmount.gt(maxFeeAmount)) {
|
|
123
|
+
rewardAmount = maxFeeAmount;
|
|
107
124
|
}
|
|
108
125
|
|
|
109
|
-
//
|
|
110
|
-
const
|
|
111
|
-
acc[x] = feeAmount.mul(new BN(shares[x])).div(RATE_BASE);
|
|
112
|
-
return acc;
|
|
113
|
-
}, {});
|
|
126
|
+
// total amount
|
|
127
|
+
const totalAmount = userAmount.add(rewardAmount);
|
|
114
128
|
|
|
115
129
|
if (stringify) {
|
|
116
130
|
return {
|
|
117
131
|
total: totalAmount.toString(10),
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
feeShares: Object.keys(feeShares).reduce((acc, x) => {
|
|
121
|
-
acc[x] = feeShares[x].toString(10);
|
|
122
|
-
return acc;
|
|
123
|
-
}, {}),
|
|
132
|
+
user: userAmount.toString(10),
|
|
133
|
+
reward: rewardAmount.toString(10),
|
|
124
134
|
};
|
|
125
135
|
}
|
|
126
136
|
|
|
127
137
|
return {
|
|
128
138
|
total: totalAmount,
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
feeShares,
|
|
139
|
+
user: userAmount,
|
|
140
|
+
reward: rewardAmount,
|
|
132
141
|
};
|
|
133
142
|
};
|
|
134
143
|
|
|
135
|
-
const
|
|
144
|
+
const splitTxFee = ({ total, shares = {}, stringify = true }) => {
|
|
145
|
+
const totalAmount = new BN(total);
|
|
146
|
+
if (totalAmount.lt(ZERO)) {
|
|
147
|
+
throw new Error('FORBIDDEN', 'Unexpected negative total when splitTxFee, abort!');
|
|
148
|
+
}
|
|
149
|
+
Object.keys(shares).forEach((key) => {
|
|
150
|
+
if (shares[key] < 0) {
|
|
151
|
+
throw new Error('FORBIDDEN', `Unexpected negative shares[${key}] when splitTxFee, abort!`);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const rewardShares = Object.keys(shares).reduce((acc, x) => {
|
|
156
|
+
acc[x] = totalAmount.mul(new BN(shares[x])).div(RATE_BASE);
|
|
157
|
+
return acc;
|
|
158
|
+
}, {});
|
|
159
|
+
|
|
160
|
+
return Object.keys(rewardShares).reduce((acc, x) => {
|
|
161
|
+
acc[x] = stringify ? rewardShares[x].toString(10) : rewardShares[x];
|
|
162
|
+
return acc;
|
|
163
|
+
}, {});
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const getRewardLocker = (rollupAddress) => toStakeAddress(rollupAddress, rollupAddress);
|
|
167
|
+
const getBNSum = (...args) => flattenDeep(args).reduce((sum, x) => sum.add(new BN(x)), new BN(0)).toString(10); // prettier-ignore
|
|
168
|
+
const isFixedFee = (x) => !x.tx.itxJson.maxFee || new BN(x.tx.itxJson.maxFee).isZero();
|
|
169
|
+
|
|
170
|
+
const ensureBlockReward = (rollupState, minReward, txStates) => {
|
|
171
|
+
const { address, withdrawFeeRate, minWithdrawFee, maxWithdrawFee, tokenAddress } = rollupState;
|
|
172
|
+
const locker = getRewardLocker(address);
|
|
173
|
+
|
|
174
|
+
const result = {
|
|
175
|
+
mintedAmount: new BN(0),
|
|
176
|
+
burnedAmount: new BN(0),
|
|
177
|
+
rewardAmount: new BN(0),
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// 0. ensure reward requirement
|
|
181
|
+
const maxPossibleReward = txStates.reduce(
|
|
182
|
+
(sum, x) => sum.add(new BN(isFixedFee(x) ? x.tx.itxJson.actualFee : x.tx.itxJson.maxFee)),
|
|
183
|
+
new BN(0)
|
|
184
|
+
);
|
|
185
|
+
const minRequiredReward = new BN(minReward);
|
|
186
|
+
if (maxPossibleReward.lt(minRequiredReward)) {
|
|
187
|
+
throw new Error('INVALID_BLOCK', 'Block reward does not match minReward requirement');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// 1. find dynamic reward tx
|
|
191
|
+
const dynamicFeeTxs = txStates.filter((x) => isFixedFee(x) === false);
|
|
192
|
+
const totalDynamicFee = dynamicFeeTxs.reduce((sum, x) => sum.add(new BN(x.tx.itxJson.maxFee)), new BN(0));
|
|
193
|
+
|
|
194
|
+
const fixedFeeTxs = txStates.filter((x) => isFixedFee(x));
|
|
195
|
+
const totalFixedFee = fixedFeeTxs.reduce((sum, x) => sum.add(new BN(x.tx.itxJson.actualFee)), new BN(0));
|
|
196
|
+
|
|
197
|
+
const totalMissingFee = minRequiredReward.sub(totalFixedFee);
|
|
198
|
+
|
|
199
|
+
// 2. calculate actual reward for each dynamic reward tx, mark tx to be updated
|
|
200
|
+
const changes = { stake: [], account: [] };
|
|
201
|
+
dynamicFeeTxs.forEach((x) => {
|
|
202
|
+
const maxFee = new BN(x.tx.itxJson.maxFee);
|
|
203
|
+
let actualFee = new BN(0);
|
|
204
|
+
// If totalMissingFee is less than 0, then the tx will be charged for fixedFee
|
|
205
|
+
if (totalMissingFee.lt(ZERO)) {
|
|
206
|
+
const fee = getTxFee({
|
|
207
|
+
amount: x.tx.itxJson.token.value,
|
|
208
|
+
feeRate: withdrawFeeRate,
|
|
209
|
+
maxFee: maxWithdrawFee,
|
|
210
|
+
minFee: minWithdrawFee,
|
|
211
|
+
stringify: false,
|
|
212
|
+
});
|
|
213
|
+
actualFee = fee.reward;
|
|
214
|
+
} else {
|
|
215
|
+
// Else the tx is charged for a portion of totalMissingFee
|
|
216
|
+
actualFee = totalMissingFee.mul(maxFee).div(totalDynamicFee);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (actualFee.lt(ZERO)) {
|
|
220
|
+
throw new Error('FORBIDDEN', 'Got negative actualFee for tx, abort!');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// If the actualFee is less than the maxFee, user will have a refund
|
|
224
|
+
if (actualFee.lt(maxFee)) {
|
|
225
|
+
const refundFee = maxFee.sub(actualFee).toString(10);
|
|
226
|
+
changes.account.push({
|
|
227
|
+
address: x.tx.from,
|
|
228
|
+
delta: refundFee,
|
|
229
|
+
action: 'refund',
|
|
230
|
+
});
|
|
231
|
+
changes.stake.push({
|
|
232
|
+
address: locker,
|
|
233
|
+
delta: `-${refundFee}`,
|
|
234
|
+
action: 'refund',
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
x.tx.itxJson.actualFee = actualFee.toString(10);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// 3. bur/mint tokens, update stakes
|
|
242
|
+
txStates.forEach((x) => {
|
|
243
|
+
const user = x.tx.itxJson.token.value;
|
|
244
|
+
const fee = x.tx.itxJson.actualFee;
|
|
245
|
+
const total = getBNSum(user, fee);
|
|
246
|
+
|
|
247
|
+
if (x.type === 'deposit_token_v2') {
|
|
248
|
+
result.rewardAmount = result.rewardAmount.add(new BN(fee));
|
|
249
|
+
result.mintedAmount = result.mintedAmount.add(new BN(total));
|
|
250
|
+
|
|
251
|
+
// mint tokens for deposit proposer
|
|
252
|
+
changes.stake.push({
|
|
253
|
+
address: toStakeAddress(x.tx.itxJson.proposer, address),
|
|
254
|
+
delta: total,
|
|
255
|
+
action: 'mint',
|
|
256
|
+
});
|
|
257
|
+
} else if (x.type === 'withdraw_token_v2') {
|
|
258
|
+
result.rewardAmount = result.rewardAmount.add(new BN(fee));
|
|
259
|
+
result.burnedAmount = result.burnedAmount.add(new BN(user));
|
|
260
|
+
|
|
261
|
+
// burn tokens from locked withdraws: user amount
|
|
262
|
+
changes.stake.push({
|
|
263
|
+
address: toStakeAddress(x.tx.from, address),
|
|
264
|
+
delta: `-${user}`,
|
|
265
|
+
action: 'burn',
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
const grouped = {
|
|
271
|
+
stake: groupBy(changes.stake, 'address'),
|
|
272
|
+
account: groupBy(changes.account, 'address'),
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
result.stakeUpdates = Object.keys(grouped.stake).reduce((acc, x) => {
|
|
276
|
+
acc[x] = {
|
|
277
|
+
address: x,
|
|
278
|
+
token: tokenAddress,
|
|
279
|
+
delta: getBNSum(...grouped.stake[x].map((c) => c.delta)),
|
|
280
|
+
action: grouped.stake[x][grouped.stake[x].length - 1].action,
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
return acc;
|
|
284
|
+
}, {});
|
|
285
|
+
|
|
286
|
+
result.accountUpdates = Object.keys(grouped.account).reduce((acc, x) => {
|
|
287
|
+
acc[x] = {
|
|
288
|
+
address: x,
|
|
289
|
+
token: tokenAddress,
|
|
290
|
+
delta: getBNSum(...grouped.account[x].map((c) => c.delta)),
|
|
291
|
+
action: grouped.account[x][grouped.account[x].length - 1].action,
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
return acc;
|
|
295
|
+
}, {});
|
|
296
|
+
|
|
297
|
+
result.mintedAmount = result.mintedAmount.toString(10);
|
|
298
|
+
result.burnedAmount = result.burnedAmount.toString(10);
|
|
299
|
+
result.rewardAmount = result.rewardAmount.toString(10);
|
|
300
|
+
|
|
301
|
+
return result;
|
|
302
|
+
};
|
|
136
303
|
|
|
137
304
|
module.exports = {
|
|
138
305
|
decodeAnySafe,
|
|
139
306
|
applyTokenUpdates,
|
|
140
307
|
applyTokenChange,
|
|
141
308
|
fixTokenInput,
|
|
142
|
-
|
|
143
|
-
|
|
309
|
+
getTxFee,
|
|
310
|
+
splitTxFee,
|
|
311
|
+
getRewardLocker,
|
|
312
|
+
ensureBlockReward,
|
|
313
|
+
getBNSum,
|
|
314
|
+
isFixedFee,
|
|
315
|
+
RATE_BASE,
|
|
144
316
|
};
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "1.13.
|
|
6
|
+
"version": "1.13.64",
|
|
7
7
|
"description": "Predefined tx pipeline sets to execute certain type of transactions",
|
|
8
8
|
"main": "lib/index.js",
|
|
9
9
|
"files": [
|
|
@@ -19,16 +19,16 @@
|
|
|
19
19
|
"author": "wangshijun <wangshijun2010@gmail.com> (http://github.com/wangshijun)",
|
|
20
20
|
"license": "MIT",
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@arcblock/did": "1.13.
|
|
23
|
-
"@arcblock/did-util": "1.13.
|
|
24
|
-
"@ocap/asset": "1.13.
|
|
25
|
-
"@ocap/mcrypto": "1.13.
|
|
26
|
-
"@ocap/merkle-tree": "1.13.
|
|
27
|
-
"@ocap/message": "1.13.
|
|
28
|
-
"@ocap/state": "1.13.
|
|
29
|
-
"@ocap/tx-pipeline": "1.13.
|
|
30
|
-
"@ocap/util": "1.13.
|
|
31
|
-
"@ocap/wallet": "1.13.
|
|
22
|
+
"@arcblock/did": "1.13.64",
|
|
23
|
+
"@arcblock/did-util": "1.13.64",
|
|
24
|
+
"@ocap/asset": "1.13.64",
|
|
25
|
+
"@ocap/mcrypto": "1.13.64",
|
|
26
|
+
"@ocap/merkle-tree": "1.13.64",
|
|
27
|
+
"@ocap/message": "1.13.64",
|
|
28
|
+
"@ocap/state": "1.13.64",
|
|
29
|
+
"@ocap/tx-pipeline": "1.13.64",
|
|
30
|
+
"@ocap/util": "1.13.64",
|
|
31
|
+
"@ocap/wallet": "1.13.64",
|
|
32
32
|
"debug": "^4.3.2",
|
|
33
33
|
"empty-value": "^1.0.1",
|
|
34
34
|
"lodash": "^4.17.21",
|
|
@@ -41,5 +41,5 @@
|
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"jest": "^27.3.1"
|
|
43
43
|
},
|
|
44
|
-
"gitHead": "
|
|
44
|
+
"gitHead": "49c94d9fd5f12ec63c34ad609d041a92f68322a8"
|
|
45
45
|
}
|