@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,176 @@
1
+ const pick = require('lodash/pick');
2
+ const { promisify } = require('util');
3
+ const isEmpty = require('empty-value');
4
+ const Joi = require('@arcblock/validator');
5
+ const Error = require('@ocap/util/lib/error');
6
+ const { Runner, pipes } = require('@ocap/tx-pipeline');
7
+ const { toStakeAddress } = require('@arcblock/did-util');
8
+ const { account, asset, stake } = require('@ocap/state');
9
+
10
+ // eslint-disable-next-line global-require
11
+ const debug = require('debug')(`${require('../../../package.json').name}:acquire-asset-v2`);
12
+
13
+ const { applyTokenUpdates } = require('../../util');
14
+
15
+ const verifyAssetOwner = promisify(pipes.VerifyUpdater({ assetKey: 'assets', ownerKey: 'owner' }));
16
+
17
+ const runner = new Runner();
18
+
19
+ // 0. verify itx
20
+ const schema = Joi.object({
21
+ address: Joi.DID().role('ROLE_STAKE').required(),
22
+ receiver: Joi.DID().required(),
23
+ locked: Joi.boolean().default(false),
24
+ message: Joi.string().trim().min(1).max(256).required(),
25
+ revokeWaitingPeriod: Joi.number().integer().min(0).default(0),
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
+ runner.use(
32
+ pipes.VerifyTxInput({
33
+ fieldKey: 'itx.inputs',
34
+ inputsKey: 'inputs',
35
+ sendersKey: 'senders',
36
+ tokensKey: 'tokens',
37
+ assetsKey: 'assets',
38
+ })
39
+ );
40
+
41
+ // 1. verify itx address
42
+ runner.use(
43
+ pipes.VerifyInfo([
44
+ {
45
+ error: 'INVALID_TX',
46
+ message: 'Invalid staking address',
47
+ fn: ({ tx, itx }) => toStakeAddress(tx.from, itx.receiver) === itx.address,
48
+ },
49
+ {
50
+ error: 'INSUFFICIENT_DATA',
51
+ message: 'Can not stake without any token or asset',
52
+ fn: ({ assets, tokens }) => !(isEmpty(tokens) && isEmpty(assets)),
53
+ },
54
+ ])
55
+ );
56
+
57
+ // 2. verify itx size: set hard limit here because more inputs leads to longer tx execute time
58
+ runner.use(pipes.VerifyListSize({ listKey: ['inputs', 'senders', 'tokens', 'assets'] }));
59
+
60
+ // 3. verify multi sig
61
+ runner.use(pipes.VerifyMultiSigV2({ signersKey: 'senders' }));
62
+
63
+ // 4. verify against state
64
+ runner.use(pipes.ExtractState({ from: 'itx.address', to: 'stakeState', status: 'OK', table: 'stake' }));
65
+
66
+ // 5. verify sender & signer & receiver
67
+ runner.use(pipes.ExtractState({ from: 'tx.from', to: 'senderState', status: 'OK', table: 'account' }));
68
+ runner.use(pipes.ExtractState({ from: 'senders', to: 'signerStates', status: 'INVALID_SIGNER_STATE', table: 'account' })); // prettier-ignore
69
+ runner.use(pipes.ExtractState({ from: 'itx.receiver', to: 'receiverState', status: 'INVALID_RECEIVER_STATE' })); // can by any type
70
+ runner.use(pipes.VerifyAccountMigration({ signerKey: 'signerStates', senderKey: 'senderState' }));
71
+
72
+ // 6. verify token state and balance
73
+ runner.use(pipes.ExtractState({ from: 'tokens', to: 'tokenStates', status: 'INVALID_TOKEN', table: 'token' }));
74
+ runner.use(pipes.VerifyTokenBalance({ ownerKey: 'signerStates', conditionKey: 'inputs' }));
75
+
76
+ // 7. verify asset state and ownership
77
+ runner.use(pipes.ExtractState({ from: 'assets', to: 'assetStates', status: 'OK', table: 'asset' }));
78
+ runner.use(pipes.VerifyTransferrable({ assets: 'assetStates' }));
79
+ runner.use(async (context, next) => {
80
+ const { inputs, assetStates = [], signerStates } = context;
81
+ for (const input of inputs) {
82
+ const { owner, assetsList } = input;
83
+ const states = assetStates.filter((x) => assetsList.includes(x.address));
84
+ const signer = signerStates.find((x) => x.address === owner);
85
+
86
+ try {
87
+ // eslint-disable-next-line no-await-in-loop
88
+ await verifyAssetOwner({ assets: states, owner: signer });
89
+ } catch (err) {
90
+ return next(err);
91
+ }
92
+ }
93
+
94
+ return next();
95
+ });
96
+
97
+ // update statedb
98
+ runner.use(
99
+ async (context, next) => {
100
+ const { tx, itx, inputs, statedb, senderState, stakeState, signerStates, assetStates = [] } = context;
101
+
102
+ const signerUpdates = {};
103
+ const stakeUpdates = stakeState
104
+ ? { tokens: stakeState.tokens, assets: stakeState.assets }
105
+ : { tokens: {}, assets: [] };
106
+
107
+ inputs.forEach((x) => {
108
+ const { owner, tokensList, assetsList } = x;
109
+ signerUpdates[owner] = applyTokenUpdates(
110
+ tokensList,
111
+ signerStates.find((s) => s.address === owner),
112
+ 'sub'
113
+ );
114
+ Object.assign(stakeUpdates, applyTokenUpdates(tokensList, stakeUpdates, 'add'));
115
+ stakeUpdates.assets.push(...assetsList);
116
+ });
117
+
118
+ const isAlsoSigner = !!signerUpdates[tx.from];
119
+
120
+ const [newSenderState, newSignerStates, newStakeState, newAssetStates] = await Promise.all([
121
+ // Update sender state
122
+ statedb.account.updateOrCreate(
123
+ senderState,
124
+ account.updateOrCreate(
125
+ senderState,
126
+ Object.assign({ address: tx.from, nonce: tx.nonce, pk: tx.pk }, signerUpdates[tx.from] || {}),
127
+ context
128
+ ),
129
+ context
130
+ ),
131
+
132
+ // Update signer state
133
+ Promise.all(
134
+ signerStates
135
+ .filter((x) => x.address !== tx.from)
136
+ .map((x) => statedb.account.update(x.address, account.update(x, signerUpdates[x.address], context), context))
137
+ ),
138
+
139
+ // Update/create stake state
140
+ stakeState
141
+ ? statedb.stake.update(stakeState.address, stake.update(stakeState, stakeUpdates, context), context)
142
+ : statedb.stake.create(
143
+ itx.address,
144
+ stake.create(
145
+ {
146
+ sender: tx.from,
147
+ revocable: !itx.locked,
148
+ ...pick(itx, ['address', 'receiver', 'message', 'data', 'revokeWaitingPeriod']),
149
+ ...stakeUpdates,
150
+ },
151
+ context
152
+ ),
153
+ context
154
+ ),
155
+
156
+ // Transfer assets to staking account
157
+ Promise.all(
158
+ assetStates.map((x) =>
159
+ statedb.asset.update(x.address, asset.update(x, { owner: itx.address }, context), context)
160
+ )
161
+ ),
162
+ ]);
163
+
164
+ context.senderState = newSenderState;
165
+ context.signerStates = isAlsoSigner ? newSignerStates.concat(newSenderState) : newSignerStates;
166
+ context.stakeState = newStakeState;
167
+ context.assetStates = newAssetStates;
168
+
169
+ debug('stake', { address: itx.address, stakeUpdates, signerUpdates });
170
+
171
+ next();
172
+ },
173
+ { persistError: true }
174
+ );
175
+
176
+ module.exports = runner;
@@ -0,0 +1,283 @@
1
+ const uniqBy = require('lodash/uniqBy');
2
+ const groupBy = require('lodash/groupBy');
3
+ const cloneDeep = require('lodash/cloneDeep');
4
+ const Joi = require('@arcblock/validator');
5
+ const Error = require('@ocap/util/lib/error');
6
+ const { BN } = require('@ocap/util');
7
+ const { Runner, pipes } = require('@ocap/tx-pipeline');
8
+ const { account, stake, evidence, rollupBlock } = require('@ocap/state');
9
+
10
+ // eslint-disable-next-line global-require
11
+ const debug = require('debug')(`${require('../../../package.json').name}:claim-block-reward`);
12
+
13
+ const { toStakeAddress } = require('@arcblock/did-util');
14
+ const VerifySigners = require('./pipes/verify-signers');
15
+ const { applyTokenChange, splitTxFee, getBNSum, getRewardLocker, RATE_BASE } = require('../../util');
16
+
17
+ const runner = new Runner();
18
+
19
+ // 1. verify itx
20
+ const schema = Joi.object({
21
+ rollup: Joi.DID().role('ROLE_ROLLUP').required(),
22
+ blockHeight: Joi.number().integer().greater(0).required(),
23
+ blockHash: Joi.string().regex(Joi.patterns.txHash).required(),
24
+ evidence: Joi.object({
25
+ hash: Joi.string().regex(Joi.patterns.txHash).required(),
26
+ }).required(),
27
+ publisher: Joi.DID().wallet('ethereum').required(),
28
+ data: Joi.any().optional(),
29
+ }).options({ stripUnknown: true, noDefaults: false });
30
+ runner.use((context, next) => {
31
+ const { tx, itx } = context;
32
+
33
+ const { error } = schema.validate(itx);
34
+ if (error) {
35
+ return next(new Error('INVALID_TX', error.message));
36
+ }
37
+
38
+ // ensure publisher same with tx.from
39
+ if (tx.from !== itx.publisher) {
40
+ return next(new Error('INVALID_TX', 'itx.publisher must be same with tx.from'));
41
+ }
42
+
43
+ context.lockerAddress = getRewardLocker(itx.rollup);
44
+
45
+ return next();
46
+ });
47
+
48
+ // 2. ensure evidence not exist
49
+ runner.use(pipes.ExtractState({ from: 'itx.evidence.hash', to: 'evidenceSeen', status: 'OK', table: 'evidence' }));
50
+ runner.use(pipes.ExtractState({ from: 'itx.blockHash', to: 'blockClaimed', status: 'OK', table: 'evidence' }));
51
+ runner.use(({ evidenceSeen, blockClaimed }, next) => {
52
+ if (evidenceSeen) return next(new Error('INVALID_TX', 'Claim evidence already seen on this chain'));
53
+ if (blockClaimed) return next(new Error('INVALID_TX', 'Block reward already claimed before this tx'));
54
+ return next();
55
+ });
56
+
57
+ // 3. verify rollup and block state
58
+ runner.use(pipes.ExtractState({ from: 'itx.rollup', to: 'rollupState', status: 'INVALID_ROLLUP', table: 'rollup' }));
59
+ runner.use(pipes.ExtractState({ from: 'itx.blockHash', to: 'blockState', status: 'INVALID_ROLLUP_BLOCK', table: 'rollupBlock' })); // prettier-ignore
60
+ runner.use((context, next) => {
61
+ const { blockState, rollupState, itx } = context;
62
+ if (blockState.rollup !== rollupState.address) {
63
+ return next(new Error('INVALID_TX', 'Rollup block belonging does not match'));
64
+ }
65
+ if (blockState.height !== itx.blockHeight) {
66
+ return next(new Error('INVALID_TX', 'Rollup block height does not match'));
67
+ }
68
+
69
+ // If the publisher is not the producer, he should wait at least rollupState.publishWaitingPeriod
70
+ if (blockState.proposer !== itx.publisher) {
71
+ const proposedAt = +new Date(blockState.context.genesisTime);
72
+ const publishedAt = +new Date(context.txTime);
73
+ if (proposedAt + rollupState.publishWaitingPeriod * 1000 > publishedAt) {
74
+ return next(new Error('INVALID_TX', 'Rollup block can only be published by producer during waiting period'));
75
+ }
76
+ }
77
+
78
+ context.producerStake = toStakeAddress(blockState.proposer, itx.rollup);
79
+
80
+ return next();
81
+ });
82
+
83
+ // 4. verify tx signers & signatures
84
+ runner.use(VerifySigners({ signersKey: 'tx.signaturesList', allowSender: true }));
85
+ runner.use(pipes.VerifyMultiSigV2({ signersKey: 'signers' }));
86
+
87
+ // 5. verify block reward locker
88
+ runner.use(pipes.ExtractState({ from: 'lockerAddress', to: 'lockerState', status: 'INVALID_LOCKER_STATE', table: 'stake' })); // prettier-ignore
89
+ runner.use(pipes.ExtractState({ from: 'producerStake', to: 'stakeState', status: 'INVALID_STAKE_STATE', table: 'stake' })); // prettier-ignore
90
+
91
+ // 6. verify sender and signer states
92
+ runner.use(pipes.ExtractState({ from: 'tx.from', to: 'senderState', status: 'INVALID_SENDER_STATE', table: 'account' })); // prettier-ignore
93
+ runner.use(pipes.ExtractState({ from: 'signers', to: 'signerStates', status: 'INVALID_SIGNER_STATE', table: 'account' })); // prettier-ignore
94
+ runner.use(pipes.VerifyAccountMigration({ signerKey: 'signerStates', senderKey: 'senderState' }));
95
+
96
+ // 7. verify token state
97
+ runner.use(pipes.ExtractState({ from: 'rollupState.tokenAddress', to: 'tokenState', status: 'INVALID_TOKEN', table: 'token' })); // prettier-ignore
98
+
99
+ // 8. split and aggregate block reward for each tx and the block
100
+ runner.use(pipes.ExtractState({ from: 'blockState.txs', to: 'txs', status: 'INVALID_TX', table: 'tx' }));
101
+ runner.use(async (context, next) => {
102
+ const { itx, txs, rollupState, blockState, lockerState, tokenState, stakeState } = context;
103
+ const { proposerFeeShare, publisherFeeShare } = rollupState;
104
+
105
+ const shares = {
106
+ publisher: publisherFeeShare,
107
+ proposer: proposerFeeShare,
108
+ validator: 10000 - publisherFeeShare - proposerFeeShare,
109
+ };
110
+
111
+ const changes = { account: [], stake: [] };
112
+
113
+ // 0. handle slash case
114
+ // TODO: the producer stake balance should be enough for slashing
115
+ const shouldSlash = blockState.proposer !== itx.publisher;
116
+ if (shouldSlash) {
117
+ const slashAmount = new BN(rollupState.minStakeAmount).mul(new BN(rollupState.publishSlashRate)).div(RATE_BASE);
118
+ const slashShare = slashAmount.div(new BN(2));
119
+ changes.stake.push({ address: stakeState.address, delta: `-${slashAmount.toString(10)}`, action: 'slash' });
120
+ changes.stake.push({ address: lockerState.address, delta: slashShare.toString(10), action: 'slash' });
121
+ changes.account.push({ address: itx.publisher, delta: slashShare.toString(10), action: 'reward' });
122
+ }
123
+
124
+ // 1. loop through the tx and split reward
125
+ txs.forEach((x) => {
126
+ const { actualFee } = x.tx.itxJson;
127
+ const { publisher, proposer, validator } = splitTxFee({ total: actualFee, shares, stringify: false });
128
+
129
+ changes.stake.push({ address: lockerState.address, delta: `-${actualFee}`, action: 'claim' });
130
+ changes.account.push({ address: itx.publisher, delta: publisher.toString(10), action: 'fee' });
131
+
132
+ if (x.type === 'deposit_token_v2') {
133
+ changes.account.push({ address: x.tx.itxJson.proposer, delta: proposer.toString(10), action: 'fee' });
134
+
135
+ // block and tx signers share the validator part
136
+ // FIXME: delegation is not supported here yet
137
+ const validators = x.tx.signatures.map((s) => s.signer).concat(blockState.signatures.map((s) => s.signer));
138
+ const validatorShare = validator.div(new BN(validators.length));
139
+ validators.forEach((v) =>
140
+ changes.account.push({ address: v, delta: validatorShare.toString(10), action: 'fee' })
141
+ );
142
+ } else if (x.type === 'withdraw_token_v2') {
143
+ changes.account.push({ address: blockState.proposer, delta: proposer.toString(10), action: 'fee' });
144
+
145
+ // block signers share the validator part
146
+ // FIXME: delegation is not supported here yet
147
+ const validators = blockState.signatures.map((s) => s.signer);
148
+ const validatorShare = validator.div(new BN(validators.length));
149
+ validators.forEach((v) =>
150
+ changes.account.push({ address: v, delta: validatorShare.toString(10), action: 'fee' })
151
+ );
152
+ }
153
+ });
154
+
155
+ // 2. aggregate changes to updates
156
+ const grouped = {
157
+ account: groupBy(changes.account, 'address'),
158
+ stake: groupBy(changes.stake, 'address'),
159
+ };
160
+
161
+ const updates = {
162
+ stake: Object.keys(grouped.stake).reduce((acc, x) => {
163
+ acc[x] = {
164
+ address: x,
165
+ token: tokenState.address,
166
+ delta: getBNSum(...grouped.stake[x].map((c) => c.delta)),
167
+ action: grouped.stake[x][grouped.stake[x].length - 1].action,
168
+ };
169
+ return acc;
170
+ }, {}),
171
+ account: Object.keys(grouped.account).reduce((acc, x) => {
172
+ acc[x] = {
173
+ address: x,
174
+ token: tokenState.address,
175
+ delta: getBNSum(...grouped.account[x].map((c) => c.delta)),
176
+ action: grouped.account[x][grouped.account[x].length - 1].action,
177
+ };
178
+ return acc;
179
+ }, {}),
180
+ };
181
+
182
+ context.changes = changes;
183
+ context.updates = updates;
184
+
185
+ return next();
186
+ });
187
+
188
+ // 9. extract all accounts that will be updated exist
189
+ runner.use((context, next) => {
190
+ const { updates, senderState, signerStates } = context;
191
+ const requiredAccounts = Object.keys(updates.account);
192
+ const knownAccounts = { [senderState.address]: senderState };
193
+
194
+ // eslint-disable-next-line no-return-assign
195
+ signerStates.forEach((x) => (knownAccounts[x.address] = x));
196
+
197
+ context.missingAccounts = requiredAccounts.filter((x) => !knownAccounts[x]);
198
+ return next();
199
+ });
200
+ runner.use(
201
+ pipes.ExtractState({
202
+ from: 'missingAccounts',
203
+ to: 'missingAccountStates',
204
+ status: 'INVALID_SIGNER_STATE',
205
+ table: 'account',
206
+ })
207
+ );
208
+ runner.use((context, next) => {
209
+ const { senderState, signerStates, missingAccountStates } = context;
210
+ context.accountStates = uniqBy([senderState, ...signerStates, ...(missingAccountStates || [])], 'address');
211
+ return next();
212
+ });
213
+
214
+ // 10. update state
215
+ runner.use(
216
+ async (context, next) => {
217
+ const { tx, itx, updates, statedb, senderState, blockState, lockerState, stakeState, accountStates } = context;
218
+
219
+ const [newStakeStates, newAccountStates, newBlockState] = await Promise.all([
220
+ // update stake states
221
+ Promise.all(
222
+ [lockerState, stakeState].map((x) =>
223
+ updates.stake[x.address]
224
+ ? statedb.stake.update(
225
+ x.address,
226
+ stake.update(x, applyTokenChange(x, updates.stake[x.address]), context),
227
+ context
228
+ )
229
+ : x
230
+ )
231
+ ),
232
+
233
+ // update accounts(proposer, publisher, validator)
234
+ Promise.all(
235
+ accountStates.map((x) =>
236
+ statedb.account.update(
237
+ x.address,
238
+ account.update(
239
+ x,
240
+ Object.assign(
241
+ x.address === senderState.address ? { nonce: tx.nonce } : {},
242
+ applyTokenChange(x, updates.account[x.address])
243
+ ),
244
+ context
245
+ ),
246
+ context
247
+ )
248
+ )
249
+ ),
250
+
251
+ // Update block state, just update the context
252
+ statedb.rollupBlock.update(blockState.hash, rollupBlock.update(blockState, context), context),
253
+
254
+ // Create evidence
255
+ statedb.evidence.create(
256
+ itx.evidence.hash,
257
+ evidence.create({ hash: itx.evidence.hash, data: 'rollup-claim-block-reward' }, context),
258
+ context
259
+ ),
260
+ statedb.evidence.create(
261
+ itx.blockHash,
262
+ evidence.create({ hash: itx.blockHash, data: 'rollup-claim-block-reward' }, context),
263
+ context
264
+ ),
265
+ ]);
266
+
267
+ // to avoid this to be incorrectly indexed
268
+ delete context.stakeState;
269
+
270
+ context.stakeStates = newStakeStates.filter((x) => updates.stake[x.address]);
271
+ context.accountStates = newAccountStates;
272
+ context.blockState = newBlockState;
273
+
274
+ context.updatedAccounts = cloneDeep([...Object.values(updates.account), ...Object.values(updates.stake)]);
275
+
276
+ debug('claim-block-reward', itx);
277
+
278
+ next();
279
+ },
280
+ { persistError: true }
281
+ );
282
+
283
+ module.exports = runner;