@ocap/tx-protocols 1.20.0 → 1.20.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/execute.js +118 -101
- 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/lib/util.js +4 -1
- package/package.json +14 -14
package/lib/execute.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
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');
|
|
5
4
|
const camelCase = require('lodash/camelCase');
|
|
@@ -9,9 +8,8 @@ const { Runner, pipes } = require('@ocap/tx-pipeline');
|
|
|
9
8
|
const createEnsureGasFn = require('./pipes/ensure-gas');
|
|
10
9
|
const createEnsureCostFn = require('./pipes/ensure-cost');
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
const ensureTxCost = promisify(createEnsureCostFn({ attachSenderChanges: true, gasOnly: true }));
|
|
11
|
+
const ensureTxGas = createEnsureGasFn(() => ({ create: 0, update: 2 }));
|
|
12
|
+
const ensureTxCost = createEnsureCostFn({ attachSenderChanges: true, gasOnly: true });
|
|
15
13
|
|
|
16
14
|
const getTxName = (typeUrl) => camelCase(typeUrl.split(':').pop());
|
|
17
15
|
|
|
@@ -50,24 +48,45 @@ module.exports = ({ filter, runAsLambda }) => {
|
|
|
50
48
|
pre.use(pipes.VerifySignature);
|
|
51
49
|
|
|
52
50
|
// charge gas fee for errored tx
|
|
53
|
-
const ensureGasFeePaid =
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
51
|
+
const ensureGasFeePaid = (context) => {
|
|
52
|
+
const gasRunner = new Runner();
|
|
53
|
+
|
|
54
|
+
gasRunner.use(ensureTxGas);
|
|
55
|
+
gasRunner.use(ensureTxCost);
|
|
56
|
+
gasRunner.use(async (ctx, next) => {
|
|
57
|
+
const { tx, statedb, senderUpdates, updateVaults } = ctx;
|
|
58
|
+
|
|
59
|
+
// If senderState does not exist and there is no stakeId for gas, an error will be thrown in ensureTxCost before here.
|
|
60
|
+
// So if there is no senderState here, it means there has a stake for gas, so allowed to continue.
|
|
61
|
+
if (ctx.senderState) {
|
|
62
|
+
const senderState = ctx.stateSnapshot?.[ctx.senderState.address] || ctx.senderState;
|
|
63
|
+
await statedb.account.update(
|
|
64
|
+
senderState.address,
|
|
65
|
+
ctx.states.account.update(senderState, { nonce: tx.nonce, pk: tx.pk, ...senderUpdates }, ctx),
|
|
66
|
+
ctx
|
|
67
|
+
);
|
|
68
|
+
await updateVaults();
|
|
69
|
+
}
|
|
57
70
|
|
|
58
|
-
|
|
71
|
+
return next();
|
|
72
|
+
});
|
|
59
73
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
context.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
74
|
+
return new Promise((resolve, reject) => {
|
|
75
|
+
gasRunner.run(context, (err) => {
|
|
76
|
+
if (err) {
|
|
77
|
+
context.logger?.error('Failed to charge gas fee for errored tx', {
|
|
78
|
+
error: err,
|
|
79
|
+
tx: context.tx,
|
|
80
|
+
txBase64: context.txBase64,
|
|
81
|
+
txHash: context.txHash,
|
|
82
|
+
});
|
|
83
|
+
reject(err);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
resolve();
|
|
88
|
+
});
|
|
89
|
+
});
|
|
71
90
|
};
|
|
72
91
|
|
|
73
92
|
// eslint-disable-next-line require-await
|
|
@@ -86,123 +105,121 @@ module.exports = ({ filter, runAsLambda }) => {
|
|
|
86
105
|
}
|
|
87
106
|
|
|
88
107
|
// eslint-disable-next-line no-shadow
|
|
89
|
-
protocol.run(context, async (
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (err) {
|
|
94
|
-
await ensureGasFeePaid(context);
|
|
108
|
+
protocol.run(context, async (error) => {
|
|
109
|
+
if (isRetrySupported) {
|
|
110
|
+
if (error) {
|
|
111
|
+
return reject(error);
|
|
95
112
|
}
|
|
113
|
+
return resolve(context);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const txStatus = error ? error.code || 'INTERNAL' : 'OK';
|
|
117
|
+
let txState = context.states.tx.create(context, txStatus);
|
|
96
118
|
|
|
97
|
-
|
|
119
|
+
if (shouldPersistTx(error)) {
|
|
98
120
|
try {
|
|
99
|
-
|
|
121
|
+
if (error) {
|
|
122
|
+
await ensureGasFeePaid(context);
|
|
123
|
+
// Recreate tx to pick up gas related fields
|
|
124
|
+
txState = context.states.tx.create(context, txStatus);
|
|
125
|
+
}
|
|
126
|
+
|
|
100
127
|
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
|
-
});
|
|
128
|
+
flushEvents(context, { txState });
|
|
107
129
|
} catch (e) {
|
|
108
|
-
context.logger?.error('Failed to save
|
|
130
|
+
context.logger?.error('Failed to save transaction to statedb', {
|
|
109
131
|
error: e,
|
|
132
|
+
txError: error,
|
|
110
133
|
txHash: context.txHash,
|
|
111
134
|
txState,
|
|
112
135
|
});
|
|
113
|
-
return
|
|
136
|
+
// If we get an internal error here, should return the original tx error to client
|
|
137
|
+
return reject(error);
|
|
114
138
|
}
|
|
115
|
-
|
|
116
|
-
flushEvents(context, { txState });
|
|
117
139
|
}
|
|
118
140
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
141
|
+
context.logger?.info('Tx finalized', {
|
|
142
|
+
txHash: context.txHash,
|
|
143
|
+
txStatus,
|
|
144
|
+
txState,
|
|
145
|
+
error,
|
|
146
|
+
});
|
|
124
147
|
|
|
125
|
-
|
|
148
|
+
if (error) {
|
|
149
|
+
return reject(error);
|
|
150
|
+
}
|
|
151
|
+
return resolve(context);
|
|
126
152
|
});
|
|
127
153
|
});
|
|
128
154
|
});
|
|
129
155
|
|
|
130
156
|
if (typeof runAsLambda === 'function') {
|
|
131
157
|
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);
|
|
158
|
+
let ctx = context;
|
|
137
159
|
const startTime = Date.now();
|
|
138
160
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
Object.defineProperty(ctx, 'txn', { value: txn });
|
|
161
|
+
try {
|
|
162
|
+
await runAsLambda(async (txn) => {
|
|
163
|
+
// create a new context each time in case we are retrying
|
|
164
|
+
ctx = pick(context, ['txBase64', 'statedb', 'indexdb', 'config', 'states', 'filter', 'extra', 'logger']);
|
|
165
|
+
ctx.txn = txn;
|
|
145
166
|
|
|
146
|
-
try {
|
|
147
167
|
await execute(ctx, protocols, true);
|
|
148
|
-
} catch (err) {
|
|
149
|
-
if (runCount <= retryLimit && shouldRetry(err)) {
|
|
150
|
-
// throw the error to retry
|
|
151
|
-
throw err;
|
|
152
|
-
}
|
|
153
|
-
error = err;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
if (error && !shouldPersistTx(error)) {
|
|
157
|
-
ctx.logger?.error('Failed to execute transaction', { error, txHash: ctx.txHash });
|
|
158
|
-
// throw the error to the abort transaction
|
|
159
|
-
throw error;
|
|
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
|
-
}
|
|
173
168
|
|
|
169
|
+
const txState = context.states.tx.create(ctx, 'OK');
|
|
174
170
|
await context.statedb.tx.create(txState.hash, txState, ctx);
|
|
171
|
+
|
|
172
|
+
// update indexdb
|
|
175
173
|
flushEvents(ctx, { txState });
|
|
176
174
|
|
|
177
175
|
ctx.logger?.info('Tx finalized', {
|
|
178
176
|
txHash: ctx.txHash,
|
|
179
|
-
txStatus,
|
|
180
177
|
txState,
|
|
181
|
-
|
|
182
|
-
runCount,
|
|
178
|
+
txStatus: 'OK',
|
|
183
179
|
duration: Date.now() - startTime,
|
|
184
180
|
});
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
}
|
|
181
|
+
});
|
|
182
|
+
} catch (error) {
|
|
183
|
+
const txStatus = error.code || 'INTERNAL';
|
|
184
|
+
let txState = context.states.tx.create(ctx, txStatus, false);
|
|
190
185
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
186
|
+
ctx.logger?.error('Failed to execute transaction', { error, txHash: ctx.txHash, txState });
|
|
187
|
+
|
|
188
|
+
if (shouldPersistTx(error)) {
|
|
189
|
+
try {
|
|
190
|
+
await runAsLambda(async (txn) => {
|
|
191
|
+
ctx = Object.assign({}, ctx, { txn, cacheStates: null });
|
|
192
|
+
|
|
193
|
+
await ensureGasFeePaid(ctx);
|
|
194
|
+
|
|
195
|
+
// Recreate tx to pick up gas related fields
|
|
196
|
+
txState = context.states.tx.create(ctx, txStatus);
|
|
199
197
|
|
|
200
|
-
|
|
201
|
-
|
|
198
|
+
await ctx.statedb.tx.create(txState.hash, txState, ctx);
|
|
199
|
+
|
|
200
|
+
// update indexdb
|
|
201
|
+
flushEvents(ctx, { txState });
|
|
202
|
+
|
|
203
|
+
ctx.logger?.info('Tx finalized', {
|
|
204
|
+
txHash: ctx.txHash,
|
|
205
|
+
txState,
|
|
206
|
+
txStatus,
|
|
207
|
+
error,
|
|
208
|
+
duration: Date.now() - startTime,
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
} catch (err) {
|
|
212
|
+
ctx.logger?.error('Failed to save invalid transaction to statedb', {
|
|
213
|
+
error: err,
|
|
214
|
+
txError: error,
|
|
215
|
+
txHash: ctx.txHash,
|
|
216
|
+
txState,
|
|
217
|
+
});
|
|
218
|
+
// If we get an error here, should return the original tx error to client
|
|
219
|
+
throw error;
|
|
220
|
+
}
|
|
202
221
|
}
|
|
203
|
-
});
|
|
204
222
|
|
|
205
|
-
if (error) {
|
|
206
223
|
throw error;
|
|
207
224
|
}
|
|
208
225
|
|
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/lib/util.js
CHANGED
|
@@ -90,7 +90,10 @@ const applyTokenUpdates = (tokens, state, operator) => {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
if (fulfilled.lt(expectedDelta)) {
|
|
93
|
-
throw new Error(
|
|
93
|
+
throw new Error(
|
|
94
|
+
'INSUFFICIENT_FUND',
|
|
95
|
+
`Negative token balance when applyTokenUpdates for ${address}, expected: ${expectedDelta.toString()} fulfilled: ${fulfilled.toString()}`
|
|
96
|
+
);
|
|
94
97
|
}
|
|
95
98
|
}
|
|
96
99
|
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "1.20.
|
|
6
|
+
"version": "1.20.2",
|
|
7
7
|
"description": "Predefined tx pipeline sets to execute certain type of transactions",
|
|
8
8
|
"main": "lib/index.js",
|
|
9
9
|
"files": [
|
|
@@ -21,18 +21,18 @@
|
|
|
21
21
|
"author": "wangshijun <wangshijun2010@gmail.com> (http://github.com/wangshijun)",
|
|
22
22
|
"license": "MIT",
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@arcblock/did": "1.20.
|
|
25
|
-
"@arcblock/did-util": "1.20.
|
|
26
|
-
"@arcblock/jwt": "1.20.
|
|
27
|
-
"@arcblock/validator": "1.20.
|
|
28
|
-
"@ocap/asset": "1.20.
|
|
29
|
-
"@ocap/mcrypto": "1.20.
|
|
30
|
-
"@ocap/merkle-tree": "1.20.
|
|
31
|
-
"@ocap/message": "1.20.
|
|
32
|
-
"@ocap/state": "1.20.
|
|
33
|
-
"@ocap/tx-pipeline": "1.20.
|
|
34
|
-
"@ocap/util": "1.20.
|
|
35
|
-
"@ocap/wallet": "1.20.
|
|
24
|
+
"@arcblock/did": "1.20.2",
|
|
25
|
+
"@arcblock/did-util": "1.20.2",
|
|
26
|
+
"@arcblock/jwt": "1.20.2",
|
|
27
|
+
"@arcblock/validator": "1.20.2",
|
|
28
|
+
"@ocap/asset": "1.20.2",
|
|
29
|
+
"@ocap/mcrypto": "1.20.2",
|
|
30
|
+
"@ocap/merkle-tree": "1.20.2",
|
|
31
|
+
"@ocap/message": "1.20.2",
|
|
32
|
+
"@ocap/state": "1.20.2",
|
|
33
|
+
"@ocap/tx-pipeline": "1.20.2",
|
|
34
|
+
"@ocap/util": "1.20.2",
|
|
35
|
+
"@ocap/wallet": "1.20.2",
|
|
36
36
|
"debug": "^4.3.6",
|
|
37
37
|
"deep-diff": "^1.0.2",
|
|
38
38
|
"empty-value": "^1.0.1",
|
|
@@ -47,5 +47,5 @@
|
|
|
47
47
|
"jest": "^29.7.0",
|
|
48
48
|
"start-server-and-test": "^1.14.0"
|
|
49
49
|
},
|
|
50
|
-
"gitHead": "
|
|
50
|
+
"gitHead": "70ddd9b43070605879fed7dff50a9f19f5325ea8"
|
|
51
51
|
}
|