@ocap/tx-protocols 1.6.3 → 1.6.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +36 -0
  2. package/lib/execute.js +97 -30
  3. package/lib/index.js +56 -14
  4. package/lib/protocols/account/declare.js +36 -30
  5. package/lib/protocols/account/delegate.js +78 -40
  6. package/lib/protocols/account/migrate.js +65 -49
  7. package/lib/protocols/account/revoke-delegate.js +39 -23
  8. package/lib/protocols/asset/acquire-v2.js +159 -0
  9. package/lib/protocols/asset/acquire-v3.js +242 -0
  10. package/lib/protocols/asset/calls/README.md +5 -0
  11. package/lib/protocols/asset/calls/transfer-token.js +37 -0
  12. package/lib/protocols/asset/calls/transfer.js +29 -0
  13. package/lib/protocols/asset/create.js +85 -36
  14. package/lib/protocols/asset/mint.js +133 -0
  15. package/lib/protocols/asset/pipes/exec-mint-hook.js +59 -0
  16. package/lib/protocols/asset/pipes/extract-factory-tokens.js +18 -0
  17. package/lib/protocols/asset/pipes/verify-acquire-params.js +30 -0
  18. package/lib/protocols/asset/pipes/verify-itx-address.js +41 -0
  19. package/lib/protocols/asset/pipes/verify-itx-assets.js +49 -0
  20. package/lib/protocols/asset/pipes/verify-itx-variables.js +26 -0
  21. package/lib/protocols/asset/pipes/verify-mint-limit.js +13 -0
  22. package/lib/protocols/asset/update.js +76 -44
  23. package/lib/protocols/factory/create.js +146 -0
  24. package/lib/protocols/governance/claim-stake.js +219 -0
  25. package/lib/protocols/governance/revoke-stake.js +136 -0
  26. package/lib/protocols/governance/stake.js +176 -0
  27. package/lib/protocols/rollup/claim-reward.js +283 -0
  28. package/lib/protocols/rollup/create-block.js +333 -0
  29. package/lib/protocols/rollup/create.js +169 -0
  30. package/lib/protocols/rollup/join.js +156 -0
  31. package/lib/protocols/rollup/leave.js +127 -0
  32. package/lib/protocols/rollup/migrate-contract.js +53 -0
  33. package/lib/protocols/rollup/migrate-token.js +66 -0
  34. package/lib/protocols/rollup/pause.js +52 -0
  35. package/lib/protocols/rollup/pipes/ensure-service-fee.js +37 -0
  36. package/lib/protocols/rollup/pipes/ensure-validator.js +10 -0
  37. package/lib/protocols/rollup/pipes/verify-evidence.js +37 -0
  38. package/lib/protocols/rollup/pipes/verify-paused.js +10 -0
  39. package/lib/protocols/rollup/pipes/verify-signers.js +88 -0
  40. package/lib/protocols/rollup/resume.js +52 -0
  41. package/lib/protocols/rollup/update.js +98 -0
  42. package/lib/protocols/token/create.js +150 -0
  43. package/lib/protocols/token/deposit-v2.js +241 -0
  44. package/lib/protocols/token/withdraw-v2.js +255 -0
  45. package/lib/protocols/trade/exchange-v2.js +179 -0
  46. package/lib/protocols/trade/transfer-v2.js +136 -0
  47. package/lib/protocols/trade/transfer-v3.js +241 -0
  48. package/lib/util.js +325 -2
  49. package/package.json +23 -16
  50. package/lib/protocols/misc/poke.js +0 -106
  51. package/lib/protocols/trade/exchange.js +0 -139
  52. package/lib/protocols/trade/transfer.js +0 -101
@@ -0,0 +1,150 @@
1
+ const isEmpty = require('empty-value');
2
+ const cloneDeep = require('lodash/cloneDeep');
3
+ const Joi = require('@arcblock/validator');
4
+ const Error = require('@ocap/util/lib/error');
5
+ const { Runner, pipes } = require('@ocap/tx-pipeline');
6
+ const { account, token } = require('@ocap/state');
7
+ const { toTokenAddress } = require('@arcblock/did-util');
8
+ const { fromTokenToUnit } = require('@ocap/util');
9
+
10
+ // eslint-disable-next-line global-require
11
+ const debug = require('debug')(`${require('../../../package.json').name}:create-token`);
12
+ const { decodeAnySafe } = require('../../util');
13
+ const ensureServiceFee = require('../rollup/pipes/ensure-service-fee');
14
+
15
+ const MAX_TOTAL_SUPPLY = fromTokenToUnit(10000 * 100000000, 18); // 32
16
+
17
+ const runner = new Runner();
18
+
19
+ runner.use(pipes.VerifyMultiSig(0));
20
+
21
+ const schema = Joi.object({
22
+ address: Joi.DID().role('ROLE_TOKEN').required(),
23
+ name: Joi.string().min(1).max(32).required(),
24
+ description: Joi.string().min(1).max(128).required(),
25
+ symbol: Joi.string().min(2).max(6).uppercase().required(),
26
+ unit: Joi.string().min(1).max(6).lowercase().required(),
27
+ decimal: Joi.number().min(6).max(18).required(),
28
+ icon: Joi.string().optional().valid(''),
29
+ totalSupply: Joi.BN().greater(0).max(MAX_TOTAL_SUPPLY).required(),
30
+ initialSupply: Joi.BN().greater(0).max(Joi.ref('totalSupply')).required(),
31
+ foreignToken: Joi.schemas.foreignToken.optional().default(null),
32
+ data: Joi.any().optional(),
33
+ }).options({ stripUnknown: true, noDefaults: false });
34
+
35
+ // 1. verify itx
36
+ runner.use(({ itx }, next) => {
37
+ const { error } = schema.validate(itx);
38
+ if (error) {
39
+ return next(new Error('INVALID_TX', `Invalid itx: ${error.message}`));
40
+ }
41
+ return next();
42
+ });
43
+
44
+ runner.use(
45
+ pipes.VerifyInfo([
46
+ {
47
+ error: 'INVALID_TOKEN',
48
+ message: 'Token address is not valid',
49
+ fn: (context) => {
50
+ const itx = cloneDeep(context.itx);
51
+ itx.data = decodeAnySafe(itx.data);
52
+ itx.address = '';
53
+
54
+ return toTokenAddress(itx) === context.itx.address;
55
+ },
56
+ },
57
+ ])
58
+ );
59
+
60
+ // Ensure sender exist
61
+ runner.use(pipes.ExtractState({ from: 'tx.from', to: 'senderState', status: 'INVALID_SENDER_STATE', table: 'account' })); // prettier-ignore
62
+ runner.use(pipes.VerifyAccountMigration({ senderKey: 'senderState' }));
63
+
64
+ // Ensure delegation
65
+ runner.use(pipes.ExtractState({ from: 'tx.delegator', to: 'delegatorState', status: 'OK', table: 'account' }));
66
+ runner.use(pipes.VerifyDelegation({ type: 'signature', signerKey: 'senderState', delegatorKey: 'delegatorState' }));
67
+
68
+ // Ensure token not exist
69
+ runner.use(pipes.ExtractState({ from: 'itx.address', to: 'tokenState', table: 'token', status: 'OK' }));
70
+ runner.use(
71
+ pipes.VerifyInfo([
72
+ {
73
+ error: 'DUPLICATE_TOKEN',
74
+ message: 'Token address already exists on chain',
75
+ fn: (context) => isEmpty(context.tokenState),
76
+ },
77
+ ])
78
+ );
79
+
80
+ // Ensure uniq ness of token symbol
81
+ runner.use(async (context, next) => {
82
+ const { symbol } = context.config.token;
83
+ if (symbol.toLowerCase() === context.itx.symbol.toLowerCase()) {
84
+ return next(new Error('DUPLICATE_SYMBOL', `Token symbol can not be ${symbol}`));
85
+ }
86
+
87
+ const exist = await context.statedb.token.existBySymbol(context.itx.symbol, context);
88
+ if (exist) {
89
+ return next(new Error('DUPLICATE_SYMBOL', 'Token symbol already exists'));
90
+ }
91
+
92
+ return next();
93
+ });
94
+
95
+ runner.use(ensureServiceFee);
96
+
97
+ // Update sender state, token state
98
+ runner.use(
99
+ async (context, next) => {
100
+ const { tx, itx, statedb, senderState, delegatorState, senderUpdates, vaultState, vaultUpdates } = context;
101
+ const data = decodeAnySafe(itx.data);
102
+ const owner = delegatorState ? delegatorState.address : senderState.address;
103
+
104
+ const delegatorUpdates = {};
105
+ senderUpdates.tokens = senderUpdates.tokens || {};
106
+
107
+ // We are definitely creating a different token, so it is safe to set tokens to initial supply
108
+ // For delegated create-token, the delegator is the actual token-holder
109
+ if (delegatorState) {
110
+ delegatorUpdates.tokens = delegatorState.tokens || {};
111
+ delegatorUpdates.tokens[itx.address] = itx.initialSupply;
112
+ } else {
113
+ senderUpdates.tokens[itx.address] = itx.initialSupply;
114
+ }
115
+
116
+ const [newSenderState, tokenState, newVaultState, newDelegatorState] = await Promise.all([
117
+ statedb.account.update(
118
+ senderState.address,
119
+ account.update(senderState, { nonce: tx.nonce, pk: tx.pk, ...senderUpdates }, context),
120
+ context
121
+ ),
122
+
123
+ statedb.token.create(itx.address, token.create({ ...cloneDeep(itx), data, issuer: owner }, context), context),
124
+
125
+ isEmpty(vaultUpdates)
126
+ ? vaultState
127
+ : statedb.account.update(vaultState.address, account.update(vaultState, vaultUpdates, context), context),
128
+
129
+ delegatorState
130
+ ? statedb.account.update(
131
+ delegatorState.address,
132
+ account.update(delegatorState, delegatorUpdates, context),
133
+ context
134
+ )
135
+ : null,
136
+ ]);
137
+
138
+ context.senderState = newSenderState;
139
+ context.tokenState = tokenState;
140
+ context.vaultState = newVaultState;
141
+ context.delegatorState = newDelegatorState;
142
+
143
+ debug('create token v2', tokenState);
144
+
145
+ next();
146
+ },
147
+ { persistError: true }
148
+ );
149
+
150
+ module.exports = runner;
@@ -0,0 +1,241 @@
1
+ /* eslint-disable indent */
2
+ const Error = require('@ocap/util/lib/error');
3
+ const Joi = require('@arcblock/validator');
4
+ const getListField = require('@ocap/util/lib/get-list-field');
5
+ const { BN, fromUnitToToken } = require('@ocap/util');
6
+ const { Runner, pipes } = require('@ocap/tx-pipeline');
7
+ const { account, stake, evidence } = require('@ocap/state');
8
+ const { toStakeAddress } = require('@arcblock/did-util');
9
+
10
+ // eslint-disable-next-line global-require
11
+ const debug = require('debug')(`${require('../../../package.json').name}:deposit-token`);
12
+
13
+ const VerifySigners = require('../rollup/pipes/verify-signers');
14
+ const VerifyPaused = require('../rollup/pipes/verify-paused');
15
+ const { applyTokenUpdates, getTxFee, getBNSum, getRewardLocker } = require('../../util');
16
+
17
+ const schema = Joi.object({
18
+ token: Joi.schemas.tokenInput.required(),
19
+ to: Joi.DID().wallet('ethereum').required(),
20
+ proposer: Joi.DID().wallet('ethereum').required(),
21
+ evidence: Joi.object({
22
+ hash: Joi.string().regex(Joi.patterns.txHash).required(),
23
+ }).required(),
24
+ rollup: Joi.DID().role('ROLE_ROLLUP').required(),
25
+ actualFee: Joi.BN().min(0).required(),
26
+ data: Joi.any().optional(),
27
+ }).options({ stripUnknown: true, noDefaults: false });
28
+
29
+ const runner = new Runner();
30
+
31
+ // 1. verify itx
32
+ runner.use(({ tx, itx }, next) => {
33
+ const { error } = schema.validate(itx);
34
+ if (error) {
35
+ return next(new Error('INVALID_TX', `Invalid itx: ${error.message}`));
36
+ }
37
+
38
+ if (itx.to !== tx.from) {
39
+ return next(new Error('INVALID_TX', 'You can only deposit to tx sender account'));
40
+ }
41
+
42
+ // ensure proposer in signer list
43
+ const signatures = getListField(tx, 'signatures');
44
+ if (signatures.some((x) => x.signer === itx.proposer) === false) {
45
+ return next(new Error('INVALID_TX', 'itx.proposer must exist in signatures'));
46
+ }
47
+
48
+ // ensure tx.from not in validator list
49
+ if (signatures.some((x) => x.signer === tx.from)) {
50
+ return next(new Error('INVALID_TX', 'tx.from should not exist in signatures'));
51
+ }
52
+
53
+ return next();
54
+ });
55
+
56
+ // 2. verify rollup against itx
57
+ runner.use(pipes.ExtractState({ from: 'itx.rollup', to: 'rollupState', status: 'INVALID_ROLLUP', table: 'rollup' }));
58
+ runner.use(VerifyPaused());
59
+ runner.use((context, next) => {
60
+ const { itx, rollupState } = context;
61
+ if (rollupState.tokenAddress !== itx.token.address) {
62
+ return next(new Error('INVALID_TX', 'Deposit token address does not match with rollup state'));
63
+ }
64
+
65
+ if (rollupState.validators.some((x) => x.address === itx.proposer) === false) {
66
+ return next(new Error('INVALID_TX', 'itx.proposer does not exist in validators'));
67
+ }
68
+
69
+ return next();
70
+ });
71
+
72
+ // 3. verify evidence
73
+ runner.use(pipes.ExtractState({ from: 'itx.evidence.hash', to: 'evidenceState', status: 'OK', table: 'evidence' }));
74
+ runner.use(({ evidenceState }, next) => {
75
+ if (evidenceState) {
76
+ return next(new Error('INVALID_TX', 'Deposit evidence already seen on this chain'));
77
+ }
78
+
79
+ return next();
80
+ });
81
+
82
+ // 4. verify signers & signatures
83
+ runner.use(VerifySigners({ signersKey: 'tx.signaturesList', allowSender: true }));
84
+ runner.use(pipes.VerifyMultiSigV2({ signersKey: 'signers' }));
85
+
86
+ // 5. verify token state
87
+ runner.use(pipes.ExtractState({ from: 'itx.token.address', to: 'tokenState', status: 'INVALID_TOKEN', table: 'token' })); // prettier-ignore
88
+
89
+ // 6. verify staking: get address, extract state, verify amount
90
+ runner.use((context, next) => {
91
+ const { itx } = context;
92
+ context.stakeAddress = toStakeAddress(itx.proposer, itx.rollup);
93
+ context.lockerAddress = getRewardLocker(itx.rollup);
94
+ return next();
95
+ });
96
+ runner.use(pipes.ExtractState({ from: 'lockerAddress', to: 'lockerState', status: 'OK', table: 'stake' }));
97
+ runner.use(pipes.ExtractState({ from: 'stakeAddress', to: 'stakeState', status: 'INVALID_STAKE_STATE', table: 'stake' })); // prettier-ignore
98
+ runner.use((context, next) => {
99
+ const { itx, stakeState, tokenState, rollupState } = context;
100
+ if (stakeState.revocable === true) {
101
+ return next(new Error('INVALID_STAKE_STATE', `Staking not locked for deposit: ${stakeState.address}`));
102
+ }
103
+
104
+ const depositAmount = new BN(itx.token.value).add(new BN(itx.actualFee));
105
+ const minDepositAmount = new BN(rollupState.minDepositAmount);
106
+ const maxDepositAmount = new BN(rollupState.maxDepositAmount);
107
+ if (depositAmount.lt(minDepositAmount)) {
108
+ return next(
109
+ new Error(
110
+ 'INVALID_TX',
111
+ `Deposit amount must be greater than minDepositAmount: ${fromUnitToToken(minDepositAmount, tokenState.decimal)}`
112
+ )
113
+ );
114
+ }
115
+ if (depositAmount.gt(maxDepositAmount)) {
116
+ return next(
117
+ new Error(
118
+ 'INVALID_TX',
119
+ `Deposit amount must be less than maxDepositAmount: ${fromUnitToToken(maxDepositAmount, tokenState.decimal)}`
120
+ )
121
+ );
122
+ }
123
+
124
+ const stakedAmount = new BN(stakeState.tokens[rollupState.tokenAddress] || 0);
125
+ if (depositAmount.gt(stakedAmount)) {
126
+ return next(
127
+ new Error(
128
+ 'INSUFFICIENT_STAKE',
129
+ `Deposit amount must be less than available stake: ${fromUnitToToken(stakedAmount, tokenState.decimal)}`
130
+ )
131
+ );
132
+ }
133
+
134
+ return next();
135
+ });
136
+
137
+ // 7. verify actualFee
138
+ runner.use((context, next) => {
139
+ const { itx, rollupState, tokenState } = context;
140
+ const { depositFeeRate, maxDepositFee, minDepositFee } = rollupState;
141
+
142
+ const { reward } = getTxFee({
143
+ amount: itx.token.value,
144
+ feeRate: depositFeeRate,
145
+ maxFee: maxDepositFee,
146
+ minFee: minDepositFee,
147
+ });
148
+
149
+ if (new BN(itx.actualFee).lt(new BN(reward))) {
150
+ const expected = fromUnitToToken(reward, tokenState.decimal);
151
+ const actual = fromUnitToToken(itx.actualFee, tokenState.decimal);
152
+ return next(new Error('INVALID_TX', `itx.actualFee too low, expect at least ${expected}, got ${actual}`));
153
+ }
154
+
155
+ return next();
156
+ });
157
+
158
+ // 7. verify sender and signer states
159
+ runner.use(pipes.ExtractState({ from: 'tx.from', to: 'senderState', status: 'OK', table: 'account' }));
160
+ runner.use(pipes.ExtractState({ from: 'signers', to: 'signerStates', status: 'INVALID_SIGNER_STATE', table: 'account' })); // prettier-ignore
161
+ runner.use(pipes.VerifyAccountMigration({ signerKey: 'signerStates', senderKey: 'senderState' }));
162
+
163
+ // 8. update state: the token minting is done when deposit finalized in rollup-block
164
+ runner.use(
165
+ async (context, next) => {
166
+ const { tx, itx, statedb, senderState, stakeState, stakeAddress, lockerState, lockerAddress } = context;
167
+
168
+ const user = itx.token.value;
169
+ const fee = itx.actualFee;
170
+ const total = getBNSum(user, fee);
171
+
172
+ const stakeUpdates = applyTokenUpdates([{ address: itx.token.address, value: total }], stakeState, 'sub');
173
+ const senderUpdates = applyTokenUpdates([{ address: itx.token.address, value: user }], senderState || {}, 'add');
174
+ const lockerUpdates = applyTokenUpdates(
175
+ [{ address: itx.token.address, value: fee }],
176
+ lockerState || { tokens: {} },
177
+ 'add'
178
+ );
179
+
180
+ const sender = senderState ? senderState.address : tx.from;
181
+ const [newSenderState, newStakeState, newLockerState, evidenceState] = await Promise.all([
182
+ // updateOrCreate user account
183
+ statedb.account.updateOrCreate(
184
+ senderState,
185
+ account.updateOrCreate(senderState, { address: sender, nonce: tx.nonce, pk: tx.pk, ...senderUpdates }, context),
186
+ context
187
+ ),
188
+
189
+ // update stake amount from the proposer
190
+ statedb.stake.update(stakeAddress, stake.update(stakeState, stakeUpdates, context), context),
191
+
192
+ // Update pending fee locker state
193
+ lockerState
194
+ ? statedb.stake.update(lockerState.address, stake.update(lockerState, lockerUpdates, context), context)
195
+ : statedb.stake.create(
196
+ lockerAddress,
197
+ stake.create(
198
+ {
199
+ address: lockerAddress,
200
+ sender: itx.rollup,
201
+ receiver: itx.rollup,
202
+ revocable: false,
203
+ message: 'pending-block-reward',
204
+ revokeWaitingPeriod: 0,
205
+ ...lockerUpdates,
206
+ },
207
+ context
208
+ ),
209
+ context
210
+ ),
211
+
212
+ // Create evidence state
213
+ statedb.evidence.create(
214
+ itx.evidence.hash,
215
+ evidence.create({ hash: itx.evidence.hash, data: 'rollup-deposit' }, context),
216
+ context
217
+ ),
218
+ ]);
219
+
220
+ context.senderState = newSenderState;
221
+ context.stakeState = newStakeState;
222
+ context.evidenceState = evidenceState;
223
+ context.stakeStates = [newStakeState, newLockerState];
224
+
225
+ context.updatedAccounts = [
226
+ // stake for tx proposer is decreased
227
+ { address: stakeAddress, token: itx.token.address, delta: `-${total}`, action: 'unlock' },
228
+ // mint to depositor from stake
229
+ { address: sender, token: itx.token.address, delta: user, action: 'unlock' },
230
+ // tx fee is locked for later claiming
231
+ { address: lockerAddress, token: itx.token.address, delta: fee, action: 'pending' },
232
+ ];
233
+
234
+ debug('deposit-token-v2', itx);
235
+
236
+ next();
237
+ },
238
+ { persistError: true }
239
+ );
240
+
241
+ module.exports = runner;
@@ -0,0 +1,255 @@
1
+ /* eslint-disable indent */
2
+ const Error = require('@ocap/util/lib/error');
3
+ const Joi = require('@arcblock/validator');
4
+ const getListField = require('@ocap/util/lib/get-list-field');
5
+ const { BN, fromUnitToToken } = require('@ocap/util');
6
+ const { Runner, pipes } = require('@ocap/tx-pipeline');
7
+ const { account, stake } = require('@ocap/state');
8
+ const { toStakeAddress } = require('@arcblock/did-util');
9
+
10
+ // eslint-disable-next-line global-require
11
+ const debug = require('debug')(`${require('../../../package.json').name}:withdraw-token`);
12
+
13
+ const VerifyPaused = require('../rollup/pipes/verify-paused');
14
+ const { applyTokenUpdates, getTxFee, getBNSum, getRewardLocker } = require('../../util');
15
+
16
+ const verifyMultiSigV2 = pipes.VerifyMultiSigV2({ signersKey: 'signers' });
17
+
18
+ const schema = Joi.object({
19
+ token: Joi.schemas.tokenInput.required(),
20
+ to: Joi.DID().wallet('ethereum').required(),
21
+ rollup: Joi.DID().role('ROLE_ROLLUP').required(),
22
+ proposer: Joi.DID().wallet('ethereum').optional().allow('').default(''),
23
+ actualFee: Joi.BN().min(0).required(),
24
+ maxFee: Joi.BN().min(0).required(),
25
+ data: Joi.any().optional(),
26
+ }).options({ stripUnknown: true, noDefaults: false });
27
+
28
+ const runner = new Runner();
29
+
30
+ // 1. verify itx
31
+ runner.use(({ tx, itx }, next) => {
32
+ const { error } = schema.validate(itx);
33
+ if (error) {
34
+ return next(new Error('INVALID_TX', `Invalid itx: ${error.message}`));
35
+ }
36
+
37
+ if (itx.to !== tx.from) {
38
+ return next(new Error('INVALID_TX', 'You can only withdraw to tx sender account'));
39
+ }
40
+
41
+ return next();
42
+ });
43
+
44
+ // 2. verify rollup against itx
45
+ runner.use(pipes.ExtractState({ from: 'itx.rollup', to: 'rollupState', status: 'INVALID_ROLLUP', table: 'rollup' }));
46
+ runner.use(VerifyPaused());
47
+ runner.use((context, next) => {
48
+ const { itx, rollupState } = context;
49
+ if (rollupState.tokenAddress !== itx.token.address) {
50
+ return next(new Error('INVALID_TX', 'Withdraw token address does not match with rollup'));
51
+ }
52
+
53
+ return next();
54
+ });
55
+
56
+ // 3. verify token state
57
+ runner.use(pipes.ExtractState({ from: 'itx.token.address', to: 'tokenState', status: 'INVALID_TOKEN', table: 'token' })); // prettier-ignore
58
+
59
+ // 4. verify sender and signer states
60
+ runner.use(pipes.ExtractState({ from: 'tx.from', to: 'senderState', status: 'INVALID_SENDER_STATE', table: 'account' })); // prettier-ignore
61
+ runner.use(pipes.VerifyAccountMigration({ senderKey: 'senderState' }));
62
+
63
+ // 5. verify amount
64
+ runner.use((context, next) => {
65
+ const { tx, itx, tokenState, rollupState } = context;
66
+
67
+ // withdraw amount
68
+ const withdrawAmount = new BN(itx.token.value);
69
+ const minWithdrawAmount = new BN(rollupState.minWithdrawAmount);
70
+ const maxWithdrawAmount = new BN(rollupState.maxWithdrawAmount);
71
+ if (withdrawAmount.lt(minWithdrawAmount)) {
72
+ return next(
73
+ new Error(
74
+ 'INVALID_TX',
75
+ `Withdraw amount must be greater than: ${fromUnitToToken(minWithdrawAmount, tokenState.decimal)}`
76
+ )
77
+ );
78
+ }
79
+ if (withdrawAmount.gt(maxWithdrawAmount)) {
80
+ return next(
81
+ new Error(
82
+ 'INVALID_TX',
83
+ `Withdraw amount must be less than maxWithdrawAmount: ${fromUnitToToken(maxWithdrawAmount, tokenState.decimal)}`
84
+ )
85
+ );
86
+ }
87
+
88
+ // user balance
89
+ const maxAmount = getBNSum(itx.token.value, itx.actualFee, itx.maxFee);
90
+ context.tokenConditions = { owner: tx.from, tokens: [{ address: itx.token.address, value: maxAmount }] };
91
+
92
+ return next();
93
+ });
94
+ runner.use(pipes.VerifyTokenBalance({ ownerKey: 'senderState', conditionKey: 'tokenConditions' }));
95
+
96
+ // 6. verify actualFee
97
+ runner.use((context, next) => {
98
+ const { itx, rollupState, tokenState } = context;
99
+ const { withdrawFeeRate, maxWithdrawFee, minWithdrawFee } = rollupState;
100
+
101
+ const isFixedFee = new BN(itx.maxFee).isZero();
102
+ if (isFixedFee) {
103
+ const { reward } = getTxFee({
104
+ amount: itx.token.value,
105
+ feeRate: withdrawFeeRate,
106
+ maxFee: maxWithdrawFee,
107
+ minFee: minWithdrawFee,
108
+ });
109
+
110
+ if (new BN(itx.actualFee).lt(new BN(reward))) {
111
+ const expected = fromUnitToToken(reward, tokenState.decimal);
112
+ const actual = fromUnitToToken(itx.actualFee, tokenState.decimal);
113
+ return next(new Error('INVALID_TX', `itx.actualFee too low, expect at least ${expected}, got ${actual}`));
114
+ }
115
+ }
116
+
117
+ return next();
118
+ });
119
+
120
+ // 7. verify maxFee and conditional multi-sig
121
+ // eslint-disable-next-line consistent-return
122
+ runner.use((context, next) => {
123
+ const { tx, itx } = context;
124
+ const isFixedFee = new BN(itx.maxFee).isZero();
125
+ if (isFixedFee) {
126
+ return next();
127
+ }
128
+
129
+ const signatures = getListField(tx, 'signatures');
130
+
131
+ // ensure signature count
132
+ if (signatures.length !== 1) {
133
+ return next(new Error('INVALID_TX', 'Withdraw with maxFee should have one multi-sig'));
134
+ }
135
+
136
+ // ensure proposer in signer list
137
+ if (signatures.some((x) => x.signer === itx.proposer) === false) {
138
+ return next(new Error('INVALID_TX', 'itx.proposer must exist in tx.signatures'));
139
+ }
140
+
141
+ // ensure signature is valid
142
+ context.signers = signatures.map((x) => x.signer);
143
+ verifyMultiSigV2(context, next);
144
+ });
145
+
146
+ // 8. verify staking: user withdraw is locked in stake
147
+ runner.use((context, next) => {
148
+ const { itx, senderState } = context;
149
+ context.stakeAddress = toStakeAddress(senderState.address, itx.rollup);
150
+ context.lockerAddress = getRewardLocker(itx.rollup);
151
+ return next();
152
+ });
153
+ runner.use(pipes.ExtractState({ from: 'lockerAddress', to: 'lockerState', status: 'OK', table: 'stake' }));
154
+ runner.use(pipes.ExtractState({ from: 'stakeAddress', to: 'stakeState', status: 'OK', table: 'stake' }));
155
+ runner.use((context, next) => {
156
+ const { stakeState } = context;
157
+ if (stakeState && stakeState.revocable === true) {
158
+ return next(new Error('INVALID_STAKE_STATE', `Staking not locked for withdraw: ${stakeState.address}`));
159
+ }
160
+
161
+ return next();
162
+ });
163
+
164
+ // 8. update state: the fee splitting and token burning is done when withdraw finalized in rollup-block
165
+ runner.use(
166
+ async (context, next) => {
167
+ const { tx, itx, statedb, stakeState, senderState, lockerState, lockerAddress, stakeAddress } = context;
168
+
169
+ const total = getBNSum(itx.token.value, itx.actualFee, itx.maxFee);
170
+ const fee = getBNSum(itx.actualFee, itx.maxFee);
171
+
172
+ const senderUpdates = applyTokenUpdates([{ address: itx.token.address, value: total }], senderState, 'sub');
173
+
174
+ // Burned amount should equal to user received amount
175
+ const stakeUpdates = applyTokenUpdates(
176
+ [{ address: itx.token.address, value: itx.token.value }],
177
+ stakeState || {},
178
+ 'add'
179
+ );
180
+
181
+ // Fees are locked to reward locker, and funded later
182
+ const lockerUpdates = applyTokenUpdates(
183
+ [{ address: itx.token.address, value: fee }],
184
+ lockerState || { tokens: {} },
185
+ 'add'
186
+ );
187
+
188
+ const [newSenderState, newStakeState, newLockerState] = await Promise.all([
189
+ // update user account
190
+ statedb.account.update(
191
+ senderState.address,
192
+ account.update(senderState, { nonce: tx.nonce, ...senderUpdates }, context),
193
+ context
194
+ ),
195
+
196
+ // lock withdraw amount to a not-revocable stake
197
+ stakeState
198
+ ? statedb.stake.update(stakeAddress, stake.update(stakeState, stakeUpdates, context), context)
199
+ : statedb.stake.create(
200
+ stakeAddress,
201
+ stake.create(
202
+ {
203
+ address: stakeAddress,
204
+ sender: senderState.address,
205
+ receiver: itx.rollup,
206
+ revocable: false,
207
+ tokens: stakeUpdates.tokens,
208
+ message: 'withdraw-locker',
209
+ assets: [],
210
+ },
211
+ context
212
+ ),
213
+ context
214
+ ),
215
+
216
+ // Update pending fee locker state
217
+ lockerState
218
+ ? statedb.stake.update(lockerState.address, stake.update(lockerState, lockerUpdates, context), context)
219
+ : statedb.stake.create(
220
+ lockerAddress,
221
+ stake.create(
222
+ {
223
+ address: lockerAddress,
224
+ sender: itx.rollup,
225
+ receiver: itx.rollup,
226
+ revocable: false,
227
+ message: 'pending-block-reward',
228
+ revokeWaitingPeriod: 0,
229
+ ...lockerUpdates,
230
+ },
231
+ context
232
+ ),
233
+ context
234
+ ),
235
+ ]);
236
+
237
+ context.senderState = newSenderState;
238
+ context.stakeState = newStakeState;
239
+ context.lockerState = newLockerState;
240
+ context.stakeStates = [newStakeState, newLockerState];
241
+
242
+ context.updatedAccounts = [
243
+ { address: senderState.address, token: itx.token.address, delta: `-${total}`, action: 'lock' },
244
+ { address: stakeAddress, token: itx.token.address, delta: itx.token.value, action: 'lock' },
245
+ { address: lockerAddress, token: itx.token.address, delta: fee, action: 'pending' },
246
+ ];
247
+
248
+ debug('withdraw-token-v2', itx);
249
+
250
+ next();
251
+ },
252
+ { persistError: true }
253
+ );
254
+
255
+ module.exports = runner;