@ocap/tx-protocols 1.20.1 → 1.20.3
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/README.md +1 -1
- package/lib/execute.js +135 -108
- package/lib/pipes/ensure-cost.js +8 -6
- package/lib/protocols/account/migrate.js +0 -1
- package/lib/protocols/governance/stake.js +0 -1
- package/package.json +26 -24
package/README.md
CHANGED
package/lib/execute.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* eslint-disable consistent-return */
|
|
2
|
-
const { promisify } = require('util');
|
|
3
2
|
const get = require('lodash/get');
|
|
4
3
|
const pick = require('lodash/pick');
|
|
4
|
+
const merge = require('lodash/merge');
|
|
5
5
|
const camelCase = require('lodash/camelCase');
|
|
6
6
|
const { CustomError: Error } = require('@ocap/util/lib/error');
|
|
7
7
|
const { Runner, pipes } = require('@ocap/tx-pipeline');
|
|
@@ -9,9 +9,8 @@ const { Runner, pipes } = require('@ocap/tx-pipeline');
|
|
|
9
9
|
const createEnsureGasFn = require('./pipes/ensure-gas');
|
|
10
10
|
const createEnsureCostFn = require('./pipes/ensure-cost');
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
const ensureTxCost = promisify(createEnsureCostFn({ attachSenderChanges: true, gasOnly: true }));
|
|
12
|
+
const ensureTxGas = createEnsureGasFn(() => ({ create: 0, update: 2 }));
|
|
13
|
+
const ensureTxCost = createEnsureCostFn({ attachSenderChanges: true, gasOnly: true });
|
|
15
14
|
|
|
16
15
|
const getTxName = (typeUrl) => camelCase(typeUrl.split(':').pop());
|
|
17
16
|
|
|
@@ -50,24 +49,45 @@ module.exports = ({ filter, runAsLambda }) => {
|
|
|
50
49
|
pre.use(pipes.VerifySignature);
|
|
51
50
|
|
|
52
51
|
// charge gas fee for errored tx
|
|
53
|
-
const ensureGasFeePaid =
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
52
|
+
const ensureGasFeePaid = (context) => {
|
|
53
|
+
const gasRunner = new Runner();
|
|
54
|
+
|
|
55
|
+
gasRunner.use(ensureTxGas);
|
|
56
|
+
gasRunner.use(ensureTxCost);
|
|
57
|
+
gasRunner.use(async (ctx, next) => {
|
|
58
|
+
const { tx, statedb, senderUpdates, updateVaults } = ctx;
|
|
59
|
+
|
|
60
|
+
// If senderState does not exist and there is no stakeId for gas, an error will be thrown in ensureTxCost before here.
|
|
61
|
+
// So if there is no senderState here, it means there has a stake for gas, so allowed to continue.
|
|
62
|
+
if (ctx.senderState) {
|
|
63
|
+
const senderState = ctx.stateSnapshot?.[ctx.senderState.address] || ctx.senderState;
|
|
64
|
+
await statedb.account.update(
|
|
65
|
+
senderState.address,
|
|
66
|
+
ctx.states.account.update(senderState, { nonce: tx.nonce, pk: tx.pk, ...senderUpdates }, ctx),
|
|
67
|
+
ctx
|
|
68
|
+
);
|
|
69
|
+
await updateVaults();
|
|
70
|
+
}
|
|
57
71
|
|
|
58
|
-
|
|
72
|
+
return next();
|
|
73
|
+
});
|
|
59
74
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
context.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
75
|
+
return new Promise((resolve, reject) => {
|
|
76
|
+
gasRunner.run(context, (err) => {
|
|
77
|
+
if (err) {
|
|
78
|
+
context.logger?.error('Failed to charge gas fee for errored tx', {
|
|
79
|
+
error: err,
|
|
80
|
+
tx: context.tx,
|
|
81
|
+
txBase64: context.txBase64,
|
|
82
|
+
txHash: context.txHash,
|
|
83
|
+
});
|
|
84
|
+
reject(err);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
resolve();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
71
91
|
};
|
|
72
92
|
|
|
73
93
|
// eslint-disable-next-line require-await
|
|
@@ -86,124 +106,131 @@ module.exports = ({ filter, runAsLambda }) => {
|
|
|
86
106
|
}
|
|
87
107
|
|
|
88
108
|
// eslint-disable-next-line no-shadow
|
|
89
|
-
protocol.run(context, async (
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (err) {
|
|
94
|
-
await ensureGasFeePaid(context);
|
|
109
|
+
protocol.run(context, async (error) => {
|
|
110
|
+
if (isRetrySupported) {
|
|
111
|
+
if (error) {
|
|
112
|
+
return reject(error);
|
|
95
113
|
}
|
|
114
|
+
return resolve(context);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const txStatus = error ? error.code || 'INTERNAL' : 'OK';
|
|
118
|
+
let txState = context.states.tx.create(context, txStatus);
|
|
96
119
|
|
|
97
|
-
|
|
120
|
+
if (shouldPersistTx(error)) {
|
|
98
121
|
try {
|
|
99
|
-
|
|
122
|
+
if (error) {
|
|
123
|
+
await ensureGasFeePaid(context);
|
|
124
|
+
// Recreate tx to pick up gas related fields
|
|
125
|
+
txState = context.states.tx.create(context, txStatus);
|
|
126
|
+
}
|
|
127
|
+
|
|
100
128
|
await context.statedb.tx.create(txState.hash, txState, context);
|
|
101
|
-
context
|
|
102
|
-
txHash: context.txHash,
|
|
103
|
-
txStatus: err ? err.code || 'INTERNAL' : 'OK',
|
|
104
|
-
txState,
|
|
105
|
-
error: err,
|
|
106
|
-
});
|
|
129
|
+
flushEvents(context, { txState });
|
|
107
130
|
} catch (e) {
|
|
108
|
-
context.logger?.error('Failed to save
|
|
131
|
+
context.logger?.error('Failed to save transaction to statedb', {
|
|
109
132
|
error: e,
|
|
133
|
+
txError: error,
|
|
110
134
|
txHash: context.txHash,
|
|
111
135
|
txState,
|
|
112
136
|
});
|
|
113
|
-
return
|
|
137
|
+
// If we get an internal error here, should return the original tx error to client
|
|
138
|
+
return reject(error);
|
|
114
139
|
}
|
|
115
|
-
|
|
116
|
-
flushEvents(context, { txState });
|
|
117
140
|
}
|
|
118
141
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
142
|
+
context.logger?.info('Tx finalized', {
|
|
143
|
+
txHash: context.txHash,
|
|
144
|
+
txStatus,
|
|
145
|
+
txState,
|
|
146
|
+
error,
|
|
147
|
+
});
|
|
124
148
|
|
|
125
|
-
|
|
149
|
+
if (error) {
|
|
150
|
+
return reject(error);
|
|
151
|
+
}
|
|
152
|
+
return resolve(context);
|
|
126
153
|
});
|
|
127
154
|
});
|
|
128
155
|
});
|
|
129
156
|
|
|
130
157
|
if (typeof runAsLambda === 'function') {
|
|
131
158
|
return async (context, protocols) => {
|
|
132
|
-
let ctx =
|
|
133
|
-
let error = null;
|
|
134
|
-
let runCount = 0;
|
|
135
|
-
const retryLimit = context.statedb?.config?.retryLimit || 0;
|
|
136
|
-
const shouldRetry = context.statedb?.config?.shouldRetry || (() => false);
|
|
159
|
+
let ctx = context;
|
|
137
160
|
const startTime = Date.now();
|
|
138
161
|
|
|
139
|
-
|
|
140
|
-
|
|
162
|
+
try {
|
|
163
|
+
const txState = await runAsLambda(
|
|
164
|
+
async (txn) => {
|
|
165
|
+
// create a new context each time in case we are retrying
|
|
166
|
+
ctx = pick(context, ['txBase64', 'statedb', 'indexdb', 'config', 'states', 'filter', 'extra', 'logger']);
|
|
167
|
+
ctx.txn = txn;
|
|
141
168
|
|
|
142
|
-
|
|
143
|
-
ctx = pick(context, ['txBase64', 'statedb', 'indexdb', 'config', 'states', 'filter', 'extra', 'logger']);
|
|
144
|
-
Object.defineProperty(ctx, 'txn', { value: txn });
|
|
169
|
+
await execute(ctx, protocols, true);
|
|
145
170
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
} catch (err) {
|
|
149
|
-
if (runCount <= retryLimit && shouldRetry(err)) {
|
|
150
|
-
// throw the error to retry
|
|
151
|
-
throw err;
|
|
152
|
-
}
|
|
153
|
-
error = err;
|
|
154
|
-
}
|
|
171
|
+
const state = context.states.tx.create(ctx, 'OK');
|
|
172
|
+
await context.statedb.tx.create(state.hash, state, ctx);
|
|
155
173
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// create tx
|
|
163
|
-
const txStatus = error ? error.code || 'INTERNAL' : 'OK';
|
|
164
|
-
try {
|
|
165
|
-
let txState = context.states.tx.create(ctx, txStatus);
|
|
166
|
-
flushEvents(ctx, { txState });
|
|
167
|
-
|
|
168
|
-
if (error) {
|
|
169
|
-
await ensureGasFeePaid(ctx);
|
|
170
|
-
// Recreate tx to pick up gas related fields
|
|
171
|
-
txState = context.states.tx.create(ctx, txStatus);
|
|
172
|
-
}
|
|
174
|
+
return state;
|
|
175
|
+
},
|
|
176
|
+
{ cleanWorkingSet: true, verifyHash: true }
|
|
177
|
+
);
|
|
173
178
|
|
|
174
|
-
|
|
175
|
-
|
|
179
|
+
// update indexdb after statedb commit
|
|
180
|
+
flushEvents(ctx, { txState });
|
|
176
181
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
182
|
+
ctx.logger?.info('Tx finalized', {
|
|
183
|
+
txHash: ctx.txHash,
|
|
184
|
+
txState,
|
|
185
|
+
txStatus: 'OK',
|
|
186
|
+
duration: Date.now() - startTime,
|
|
187
|
+
});
|
|
188
|
+
} catch (error) {
|
|
189
|
+
const txStatus = error.code || 'INTERNAL';
|
|
190
|
+
let txState = ctx.tx ? context.states.tx.create(ctx, txStatus, false) : null;
|
|
191
|
+
|
|
192
|
+
ctx.logger?.error('Failed to execute transaction', { error, txHash: ctx.txHash, txState });
|
|
193
|
+
|
|
194
|
+
if (txState && shouldPersistTx(error)) {
|
|
195
|
+
try {
|
|
196
|
+
txState = await runAsLambda(async (txn) => {
|
|
197
|
+
ctx = Object.assign({}, ctx, { txn, cacheStates: null });
|
|
198
|
+
|
|
199
|
+
await ensureGasFeePaid(ctx);
|
|
200
|
+
|
|
201
|
+
// Recreate tx to pick up gas related fields
|
|
202
|
+
const state = context.states.tx.create(ctx, txStatus);
|
|
203
|
+
await ctx.statedb.tx.create(state.hash, state, ctx);
|
|
204
|
+
|
|
205
|
+
return state;
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// update indexdb after statedb commit
|
|
209
|
+
flushEvents(ctx, { txState });
|
|
210
|
+
|
|
211
|
+
ctx.logger?.info('Tx finalized', {
|
|
212
|
+
txHash: ctx.txHash,
|
|
213
|
+
txState,
|
|
214
|
+
txStatus,
|
|
215
|
+
error,
|
|
216
|
+
duration: Date.now() - startTime,
|
|
217
|
+
});
|
|
218
|
+
} catch (err) {
|
|
219
|
+
ctx.logger?.error('Failed to save invalid transaction to statedb', {
|
|
220
|
+
error: err,
|
|
221
|
+
txError: error,
|
|
222
|
+
txHash: ctx.txHash,
|
|
223
|
+
txState,
|
|
224
|
+
});
|
|
225
|
+
// If we get an error here, should return the original tx error to client
|
|
226
|
+
throw error;
|
|
189
227
|
}
|
|
190
|
-
|
|
191
|
-
const txState = context.states.tx.create(ctx, txStatus, false);
|
|
192
|
-
ctx.logger?.error('Failed to save invalid transaction to statedb', {
|
|
193
|
-
error: err,
|
|
194
|
-
protocolError: error,
|
|
195
|
-
txHash: ctx.txHash,
|
|
196
|
-
txState,
|
|
197
|
-
duration: Date.now() - startTime,
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
// throw error to abort transaction
|
|
201
|
-
throw err;
|
|
202
228
|
}
|
|
203
|
-
});
|
|
204
229
|
|
|
205
|
-
if (error) {
|
|
206
230
|
throw error;
|
|
231
|
+
} finally {
|
|
232
|
+
// Merge new ctx to original context
|
|
233
|
+
merge(context, ctx);
|
|
207
234
|
}
|
|
208
235
|
|
|
209
236
|
return ctx;
|
package/lib/pipes/ensure-cost.js
CHANGED
|
@@ -24,7 +24,7 @@ module.exports = function CreateEnsureTxCostPipe({
|
|
|
24
24
|
} = {}) {
|
|
25
25
|
return async function EnsureTxCost(context, next) {
|
|
26
26
|
// TODO: we are using the sender as gas payer, this may change in future
|
|
27
|
-
const { config, statedb, txType, senderState, gasEstimate,
|
|
27
|
+
const { config, statedb, txType, senderState, gasEstimate, totalGas } = context;
|
|
28
28
|
|
|
29
29
|
// verify gas staking headers
|
|
30
30
|
const { tx, extra = {}, txHash } = context;
|
|
@@ -83,11 +83,11 @@ module.exports = function CreateEnsureTxCostPipe({
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
let isCostCharged = false;
|
|
86
|
-
if (
|
|
86
|
+
if (txCost.gt(ZERO)) {
|
|
87
87
|
const expected = new BN(gasEstimate.payment || 0).add(txCost);
|
|
88
|
-
const actual = new BN(senderState
|
|
88
|
+
const actual = new BN(senderState?.tokens?.[config.token.address] || 0);
|
|
89
89
|
// If we have someone with enough balance to pay for this tx
|
|
90
|
-
if (actual.gte(expected)) {
|
|
90
|
+
if (senderState && actual.gte(expected)) {
|
|
91
91
|
isCostCharged = true;
|
|
92
92
|
|
|
93
93
|
// to be merged into later pipe
|
|
@@ -108,7 +108,7 @@ module.exports = function CreateEnsureTxCostPipe({
|
|
|
108
108
|
context.updateVaults = async function updateVaults() {
|
|
109
109
|
const [feeVaultState, gasVaultState] = await Promise.all([
|
|
110
110
|
changes.fee ? statedb.account.get(context.feeVault, context) : null,
|
|
111
|
-
changes.gas ? statedb.account.get(gasVault, context) : null,
|
|
111
|
+
changes.gas ? statedb.account.get(context.gasVault, context) : null,
|
|
112
112
|
]);
|
|
113
113
|
|
|
114
114
|
const [newFeeVaultState, newGasVaultState] = await Promise.all([
|
|
@@ -167,11 +167,13 @@ module.exports = function CreateEnsureTxCostPipe({
|
|
|
167
167
|
|
|
168
168
|
context.gasPaid = true;
|
|
169
169
|
};
|
|
170
|
+
} else if (!senderState) {
|
|
171
|
+
return next(new Error('INVALID_GAS_PAYER', `Gas payer ${tx.from} does not exist on chain`));
|
|
170
172
|
} else if (throwOnInsufficientFund) {
|
|
171
173
|
return next(
|
|
172
174
|
new Error(
|
|
173
175
|
'INSUFFICIENT_FUND',
|
|
174
|
-
`Insufficient fund to pay for tx cost from ${senderState.
|
|
176
|
+
`Insufficient fund to pay for tx cost from ${senderState?.address || tx.from}, expected ${fromUnitToToken(
|
|
175
177
|
expected,
|
|
176
178
|
config.token.decimal
|
|
177
179
|
)}, got ${fromUnitToToken(actual, config.token.decimal)}`
|
package/package.json
CHANGED
|
@@ -3,41 +3,36 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "1.20.
|
|
6
|
+
"version": "1.20.3",
|
|
7
7
|
"description": "Predefined tx pipeline sets to execute certain type of transactions",
|
|
8
8
|
"main": "lib/index.js",
|
|
9
9
|
"files": [
|
|
10
10
|
"lib"
|
|
11
11
|
],
|
|
12
|
-
"scripts": {
|
|
13
|
-
"lint": "eslint tests lib",
|
|
14
|
-
"lint:fix": "eslint --fix tests lib",
|
|
15
|
-
"start": "node tools/start-chain.js",
|
|
16
|
-
"test": "jest --forceExit --detectOpenHandles",
|
|
17
|
-
"test:ci": "jest --forceExit --detectOpenHandles --coverage",
|
|
18
|
-
"coverage": "start-server-and-test start http://127.0.0.1:4001 test:ci"
|
|
19
|
-
},
|
|
20
12
|
"keywords": [],
|
|
21
13
|
"author": "wangshijun <wangshijun2010@gmail.com> (http://github.com/wangshijun)",
|
|
22
14
|
"license": "MIT",
|
|
23
15
|
"dependencies": {
|
|
24
|
-
"@arcblock/did": "1.20.1",
|
|
25
|
-
"@arcblock/did-util": "1.20.1",
|
|
26
|
-
"@arcblock/jwt": "1.20.1",
|
|
27
|
-
"@arcblock/validator": "1.20.1",
|
|
28
|
-
"@ocap/asset": "1.20.1",
|
|
29
|
-
"@ocap/mcrypto": "1.20.1",
|
|
30
|
-
"@ocap/merkle-tree": "1.20.1",
|
|
31
|
-
"@ocap/message": "1.20.1",
|
|
32
|
-
"@ocap/state": "1.20.1",
|
|
33
|
-
"@ocap/tx-pipeline": "1.20.1",
|
|
34
|
-
"@ocap/util": "1.20.1",
|
|
35
|
-
"@ocap/wallet": "1.20.1",
|
|
36
16
|
"debug": "^4.3.6",
|
|
37
17
|
"deep-diff": "^1.0.2",
|
|
38
18
|
"empty-value": "^1.0.1",
|
|
39
19
|
"lodash": "^4.17.21",
|
|
40
|
-
"url-join": "^4.0.1"
|
|
20
|
+
"url-join": "^4.0.1",
|
|
21
|
+
"@arcblock/did": "1.20.3",
|
|
22
|
+
"@arcblock/did-util": "1.20.3",
|
|
23
|
+
"@arcblock/jwt": "1.20.3",
|
|
24
|
+
"@arcblock/validator": "1.20.3",
|
|
25
|
+
"@ocap/asset": "1.20.3",
|
|
26
|
+
"@ocap/mcrypto": "1.20.3",
|
|
27
|
+
"@ocap/merkle-tree": "1.20.3",
|
|
28
|
+
"@ocap/message": "1.20.3",
|
|
29
|
+
"@ocap/state": "1.20.3",
|
|
30
|
+
"@ocap/util": "1.20.3",
|
|
31
|
+
"@ocap/wallet": "1.20.3",
|
|
32
|
+
"@ocap/tx-pipeline": "1.20.3",
|
|
33
|
+
"@ocap/e2e-test": "1.20.3",
|
|
34
|
+
"@ocap/client": "1.20.3",
|
|
35
|
+
"@ocap/statedb-memory": "1.20.3"
|
|
41
36
|
},
|
|
42
37
|
"resolutions": {
|
|
43
38
|
"bn.js": "5.2.1",
|
|
@@ -47,5 +42,12 @@
|
|
|
47
42
|
"jest": "^29.7.0",
|
|
48
43
|
"start-server-and-test": "^1.14.0"
|
|
49
44
|
},
|
|
50
|
-
"
|
|
51
|
-
|
|
45
|
+
"scripts": {
|
|
46
|
+
"lint": "eslint tests lib",
|
|
47
|
+
"lint:fix": "eslint --fix tests lib",
|
|
48
|
+
"start": "node tools/start-chain.js",
|
|
49
|
+
"test": "jest --forceExit --detectOpenHandles",
|
|
50
|
+
"test:ci": "jest --forceExit --detectOpenHandles --coverage",
|
|
51
|
+
"coverage": "start-server-and-test start http://127.0.0.1:4001 test:ci"
|
|
52
|
+
}
|
|
53
|
+
}
|