@aeternity/aepp-sdk 14.1.1 → 15.0.1
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/dist/aepp-sdk.browser-script.cjs +1 -1
- package/dist/aepp-sdk.browser-script.cjs.map +1 -1
- package/dist/aepp-sdk.browser.cjs +1 -1
- package/dist/aepp-sdk.browser.cjs.map +1 -1
- package/dist/aepp-sdk.cjs +3112 -506
- package/dist/aepp-sdk.cjs.map +1 -1
- package/es/account/Base.d.ts +6 -1
- package/es/account/Base.js.map +1 -1
- package/es/account/Generalized.d.ts +1 -1
- package/es/account/Generalized.js +7 -1
- package/es/account/Generalized.js.map +1 -1
- package/es/apis/node/models/index.d.ts +405 -79
- package/es/apis/node/models/index.js.map +1 -1
- package/es/apis/node/models/mappers.d.ts +54 -4
- package/es/apis/node/models/mappers.js +1910 -500
- package/es/apis/node/models/mappers.js.map +1 -1
- package/es/apis/node/node.d.ts +28 -6
- package/es/apis/node/node.js +276 -5
- package/es/apis/node/node.js.map +1 -1
- package/es/channel/Contract.d.ts +5 -0
- package/es/channel/Contract.js +8 -2
- package/es/channel/Contract.js.map +1 -1
- package/es/channel/handlers.js +3 -3
- package/es/channel/handlers.js.map +1 -1
- package/es/contract/ga.d.ts +5 -2
- package/es/contract/ga.js +53 -21
- package/es/contract/ga.js.map +1 -1
- package/es/index-browser.d.ts +3 -1
- package/es/index-browser.js +2 -1
- package/es/index-browser.js.map +1 -1
- package/es/tx/builder/constants.d.ts +13 -2
- package/es/tx/builder/constants.js +12 -1
- package/es/tx/builder/constants.js.map +1 -1
- package/es/tx/builder/entry/index.js +6 -0
- package/es/tx/builder/entry/index.js.map +1 -1
- package/es/tx/builder/entry/schema.d.ts +10 -10
- package/es/tx/builder/field-types/abi-version.js +6 -4
- package/es/tx/builder/field-types/abi-version.js.map +1 -1
- package/es/tx/builder/field-types/ct-version.d.ts +19 -0
- package/es/tx/builder/field-types/ct-version.js +27 -1
- package/es/tx/builder/field-types/ct-version.js.map +1 -1
- package/es/tx/builder/field-types/fee.d.ts +9 -7
- package/es/tx/builder/field-types/fee.js +75 -66
- package/es/tx/builder/field-types/fee.js.map +1 -1
- package/es/tx/builder/field-types/gas-limit.d.ts +5 -3
- package/es/tx/builder/field-types/gas-limit.js +25 -14
- package/es/tx/builder/field-types/gas-limit.js.map +1 -1
- package/es/tx/builder/field-types/gas-price.d.ts +6 -4
- package/es/tx/builder/field-types/gas-price.js +91 -21
- package/es/tx/builder/field-types/gas-price.js.map +1 -1
- package/es/tx/builder/field-types/interface.d.ts +20 -0
- package/es/tx/builder/field-types/interface.js +17 -1
- package/es/tx/builder/field-types/interface.js.map +1 -1
- package/es/tx/builder/field-types/name-fee.d.ts +3 -2
- package/es/tx/builder/field-types/name-fee.js +6 -0
- package/es/tx/builder/field-types/name-fee.js.map +1 -1
- package/es/tx/builder/field-types/pointers.d.ts +2 -1
- package/es/tx/builder/field-types/pointers.js +6 -2
- package/es/tx/builder/field-types/pointers.js.map +1 -1
- package/es/tx/builder/field-types/transaction.d.ts +4 -1
- package/es/tx/builder/field-types/transaction.js +18 -3
- package/es/tx/builder/field-types/transaction.js.map +1 -1
- package/es/tx/builder/field-types/with-formatting.d.ts +1 -1
- package/es/tx/builder/field-types/with-formatting.js +1 -1
- package/es/tx/builder/field-types/with-formatting.js.map +1 -1
- package/es/tx/builder/index.d.ts +13 -0
- package/es/tx/builder/index.js +92 -5
- package/es/tx/builder/index.js.map +1 -1
- package/es/tx/builder/protocol-parameters.d.ts +134 -0
- package/es/tx/builder/protocol-parameters.js +539 -0
- package/es/tx/builder/protocol-parameters.js.map +1 -0
- package/es/tx/builder/schema.d.ts +156 -155
- package/es/tx/builder/schema.js +5 -1
- package/es/tx/builder/schema.js.map +1 -1
- package/es/tx/execution-cost.js +5 -5
- package/es/tx/execution-cost.js.map +1 -1
- package/es/tx/transaction-signer.js +2 -2
- package/es/tx/transaction-signer.js.map +1 -1
- package/es/tx/validator.js +4 -4
- package/es/tx/validator.js.map +1 -1
- package/es/utils/wrap-proxy.d.ts +10 -0
- package/es/utils/wrap-proxy.js +18 -0
- package/es/utils/wrap-proxy.js.map +1 -1
- package/package.json +15 -3
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
import { AbiVersion, MAX_AUTH_FUN_GAS, MIN_GAS_PRICE, Tag } from './constants.js';
|
|
2
|
+
import { isKeyOfObject } from '../../utils/other.js';
|
|
3
|
+
import { ArgumentError, InternalError, NodeError } from '../../utils/errors.js';
|
|
4
|
+
import { unwrapProxy } from '../../utils/wrap-proxy.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Consensus parameters and node policy settings the transaction builder needs to produce a
|
|
8
|
+
* transaction the node would accept — see {@link getCachedProtocolParameters}.
|
|
9
|
+
* @category transaction builder
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Transaction option to build a transaction against specific consensus parameters. `buildTxAsync`
|
|
14
|
+
* sets it to the parameters it requests from node, provide it to build a transaction offline for a
|
|
15
|
+
* node that doesn't run the protocol version the current SDK release was made for.
|
|
16
|
+
* @category transaction builder
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const BASE_GAS = 15000;
|
|
20
|
+
const defaultStateGasPerBlock = {
|
|
21
|
+
part: 32000,
|
|
22
|
+
whole: Math.floor(60 * 24 * 365 / 3)
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Protocol parameters are shared between every transaction built against them, and
|
|
27
|
+
* {@link defaultProtocolParameters} is shared process-wide. `readonly` guards them at compile time
|
|
28
|
+
* only, so freeze them to make sure a transaction can't be mispriced by an unrelated component
|
|
29
|
+
* mutating parameters it doesn't own.
|
|
30
|
+
*/
|
|
31
|
+
function freezeParameters(parameters) {
|
|
32
|
+
Object.values(parameters.contractTxBaseGas).forEach(Object.freeze);
|
|
33
|
+
Object.values(parameters.stateGasPerBlock).forEach(Object.freeze);
|
|
34
|
+
Object.freeze(parameters.txBaseGas);
|
|
35
|
+
Object.freeze(parameters.contractTxBaseGas);
|
|
36
|
+
Object.freeze(parameters.stateGasPerBlock);
|
|
37
|
+
return Object.freeze(parameters);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Protocol parameters of Ceres as they were at the moment of the SDK release. Used to build
|
|
42
|
+
* transactions without a node connection, and on nodes that don't provide the protocol parameters
|
|
43
|
+
* endpoint yet.
|
|
44
|
+
* @category transaction builder
|
|
45
|
+
* @see {@link https://github.com/aeternity/protocol/blob/master/consensus/README.md#gas}
|
|
46
|
+
*/
|
|
47
|
+
export const defaultProtocolParameters = freezeParameters({
|
|
48
|
+
minGasPrice: BigInt(MIN_GAS_PRICE),
|
|
49
|
+
minMinerGasPrice: BigInt(MIN_GAS_PRICE),
|
|
50
|
+
gasPerByte: 20,
|
|
51
|
+
blockGasLimit: 6e6,
|
|
52
|
+
maxAuthFunGas: MAX_AUTH_FUN_GAS,
|
|
53
|
+
txBaseGas: {
|
|
54
|
+
...Object.fromEntries(Object.values(Tag).filter(tag => typeof tag === 'number')
|
|
55
|
+
// node reports the base gas of contract-executing types in `contract_tx_base_gas`,
|
|
56
|
+
// `SignedTx` is not a chargeable transaction type
|
|
57
|
+
.filter(tag => ![Tag.SignedTx, Tag.ContractCreateTx, Tag.ContractCallTx, Tag.GaAttachTx, Tag.GaMetaTx].includes(tag)).map(tag => [tag, BASE_GAS])),
|
|
58
|
+
[Tag.ChannelForceProgressTx]: 30 * BASE_GAS,
|
|
59
|
+
[Tag.ChannelOffChainTx]: 0,
|
|
60
|
+
[Tag.PayingForTx]: BASE_GAS / 5
|
|
61
|
+
},
|
|
62
|
+
contractTxBaseGas: {
|
|
63
|
+
[Tag.ContractCreateTx]: {
|
|
64
|
+
[AbiVersion.Sophia]: 5 * BASE_GAS,
|
|
65
|
+
[AbiVersion.Fate]: 5 * BASE_GAS
|
|
66
|
+
},
|
|
67
|
+
[Tag.ContractCallTx]: {
|
|
68
|
+
[AbiVersion.Sophia]: 30 * BASE_GAS,
|
|
69
|
+
[AbiVersion.Fate]: 12 * BASE_GAS
|
|
70
|
+
},
|
|
71
|
+
[Tag.GaAttachTx]: {
|
|
72
|
+
[AbiVersion.Sophia]: 5 * BASE_GAS,
|
|
73
|
+
[AbiVersion.Fate]: 5 * BASE_GAS
|
|
74
|
+
},
|
|
75
|
+
[Tag.GaMetaTx]: {
|
|
76
|
+
[AbiVersion.Sophia]: 5 * BASE_GAS,
|
|
77
|
+
[AbiVersion.Fate]: 5 * BASE_GAS
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
stateGasPerBlock: Object.fromEntries([Tag.OracleRegisterTx, Tag.OracleExtendTx, Tag.OracleQueryTx, Tag.OracleRespondTx].map(tag => [tag, {
|
|
81
|
+
...defaultStateGasPerBlock
|
|
82
|
+
}]))
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// node names transaction types after `aetx:type_to_swagger_name/1`, it matches `Tag` except of these
|
|
86
|
+
const txTypeToTag = {
|
|
87
|
+
GAAttachTx: Tag.GaAttachTx,
|
|
88
|
+
GAMetaTx: Tag.GaMetaTx
|
|
89
|
+
};
|
|
90
|
+
function getTag(txType) {
|
|
91
|
+
// node controls these keys, `isKeyOfObject` also accepts anything inherited from
|
|
92
|
+
// `Object.prototype` (`constructor`, `toString`) — a name of a transaction type is neither
|
|
93
|
+
const isOwnKeyOf = object => Object.prototype.hasOwnProperty.call(object, txType);
|
|
94
|
+
if (isOwnKeyOf(txTypeToTag) && isKeyOfObject(txType, txTypeToTag)) return txTypeToTag[txType];
|
|
95
|
+
if (!isOwnKeyOf(Tag) || !isKeyOfObject(txType, Tag)) return undefined;
|
|
96
|
+
// `Tag` is a numeric enum, so it is also indexable by a member value (`Tag['12'] === 'SpendTx'`)
|
|
97
|
+
const tag = Tag[txType];
|
|
98
|
+
return typeof tag === 'number' ? tag : undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Node decides these values and they multiply into the fee the user pays, so a node that is
|
|
102
|
+
// compromised, misconfigured, or impersonated could make the SDK build a transaction with an
|
|
103
|
+
// extreme fee. A hard fork may legitimately raise them, but not by orders of magnitude — refuse to
|
|
104
|
+
// build against a value far above the one the SDK was released with rather than silently overpay.
|
|
105
|
+
const maxRaiseFactor = 1000;
|
|
106
|
+
function excessiveError(name, value, limit) {
|
|
107
|
+
return new NodeError(`Node reports ${name} as ${String(value)}, which is outside of the range the SDK considers` + ` plausible (0..${limit}). Provide \`protocolParameters\` in options to build a transaction` + ' against these parameters anyway.');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* A value that doesn't match the schema node declares for it — the api client passes `null`,
|
|
112
|
+
* booleans, strings, and fractions through unchanged. Not a {@link NodeError}: the parameters are
|
|
113
|
+
* unreadable rather than implausible, so {@link getCachedProtocolParameters} falls back to
|
|
114
|
+
* {@link defaultProtocolParameters}, as for a node without the endpoint (which a node can force
|
|
115
|
+
* with a 404 anyway).
|
|
116
|
+
*/
|
|
117
|
+
function malformedError(name, value) {
|
|
118
|
+
return new InternalError(`Node reports ${name} as ${String(value)}, which doesn't match the schema it declares for it`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Checks a value node reports against the one the SDK was released with, returns the factor it is
|
|
123
|
+
* raised by.
|
|
124
|
+
*/
|
|
125
|
+
function checkNotExcessive(name, value, sdkValue) {
|
|
126
|
+
// a parameter the SDK release has at 0 can only stay at 0, it raises the fee by nothing
|
|
127
|
+
const raise = number => sdkValue === 0 ? 1 : number / sdkValue;
|
|
128
|
+
const limit = sdkValue * maxRaiseFactor;
|
|
129
|
+
// a bigint matches the schema whatever its magnitude, so one out of range is an implausible
|
|
130
|
+
// value rather than an unreadable one. Compared as a bigint: a value far enough above the limit
|
|
131
|
+
// can't be counted in a number, and that is what makes it implausible in the first place
|
|
132
|
+
if (typeof value === 'bigint') {
|
|
133
|
+
if (value < 0n || value > BigInt(limit)) throw excessiveError(name, value, limit);
|
|
134
|
+
return raise(Number(value));
|
|
135
|
+
}
|
|
136
|
+
if (!Number.isSafeInteger(value)) throw malformedError(name, value);
|
|
137
|
+
if (value < 0 || value > limit) throw excessiveError(name, value, limit);
|
|
138
|
+
return raise(value);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* `Math.max(...array)` passes every element as an argument and overflows the stack on an array
|
|
143
|
+
* node can make arbitrarily long — it reports one entry per transaction type and abi version.
|
|
144
|
+
*/
|
|
145
|
+
export function maxOf(numbers) {
|
|
146
|
+
return numbers.reduce((max, number) => number > max ? number : max, -Infinity);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// TODO: express the bound in aettos instead — a defaulted value may not commit the user to more
|
|
150
|
+
// than a fixed coin amount — rather than as a ratio against the parameters of the SDK release.
|
|
151
|
+
// That states the actual requirement directly and holds on any network, but it defaults a higher
|
|
152
|
+
// gas limit on a network that raised the block gas limit, so it is a behavior change, not a
|
|
153
|
+
// refactor [behavior change on networks running other parameters]
|
|
154
|
+
/**
|
|
155
|
+
* How much node's parameters raise each product a transaction costs, against the SDK release.
|
|
156
|
+
* Kept out of {@link ProtocolParameters} because they are not consensus parameters but a property
|
|
157
|
+
* of where the parameters came from — the ones a caller provides in options don't go through here.
|
|
158
|
+
*
|
|
159
|
+
* One per gas amount the SDK picks itself, and not one per parameter: each is charged to the value
|
|
160
|
+
* it bounds, so that a raise of one doesn't shrink a value it has nothing to do with.
|
|
161
|
+
*/
|
|
162
|
+
|
|
163
|
+
const parameterRaises = new WeakMap();
|
|
164
|
+
|
|
165
|
+
// parameters this process built itself are checked where they are built — the ones of the SDK
|
|
166
|
+
// release by construction, the ones node reports by `checkParametersNotExcessive`. This remembers
|
|
167
|
+
// the caller-provided ones already walked, a build reads them once per field and per rebuild pass
|
|
168
|
+
const usableParameters = new WeakSet();
|
|
169
|
+
function checkUsableGas(name, value) {
|
|
170
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
171
|
+
throw new ArgumentError(`protocolParameters.${name}`, 'a non-negative safe integer', value);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Checks that parameters a caller provided in options can price a transaction at all — only values
|
|
177
|
+
* that would produce `NaN`, `Infinity`, or a division by zero, not their magnitude: options are the
|
|
178
|
+
* way out of the bounds {@link convertProtocolParameters} applies, bounding them here would leave
|
|
179
|
+
* no way to build against a network the SDK release doesn't know. They are as trusted as `fee` and
|
|
180
|
+
* `gasLimit`: must not come from untrusted input, this only turns an unusable value into an error
|
|
181
|
+
* naming the field. See `serializeAsIsParam`.
|
|
182
|
+
* @param parameters - Parameters provided in options
|
|
183
|
+
*/
|
|
184
|
+
export function checkParametersUsable(parameters) {
|
|
185
|
+
// built by this process, or already walked
|
|
186
|
+
if (parameterRaises.has(parameters) || usableParameters.has(parameters)) return;
|
|
187
|
+
if (typeof parameters.minGasPrice !== 'bigint' || parameters.minGasPrice < 1n) {
|
|
188
|
+
throw new ArgumentError('protocolParameters.minGasPrice', 'a positive bigint (a transaction can’t be priced against 0)', parameters.minGasPrice);
|
|
189
|
+
}
|
|
190
|
+
if (typeof parameters.minMinerGasPrice !== 'bigint' || parameters.minMinerGasPrice < 0n) {
|
|
191
|
+
throw new ArgumentError('protocolParameters.minMinerGasPrice', 'a non-negative bigint', parameters.minMinerGasPrice);
|
|
192
|
+
}
|
|
193
|
+
checkUsableGas('gasPerByte', parameters.gasPerByte);
|
|
194
|
+
checkUsableGas('blockGasLimit', parameters.blockGasLimit);
|
|
195
|
+
checkUsableGas('maxAuthFunGas', parameters.maxAuthFunGas);
|
|
196
|
+
Object.entries(parameters.txBaseGas).forEach(([tag, gas]) => {
|
|
197
|
+
var _Tag$tag;
|
|
198
|
+
return checkUsableGas(`txBaseGas.${(_Tag$tag = Tag[+tag]) !== null && _Tag$tag !== void 0 ? _Tag$tag : tag}`, gas);
|
|
199
|
+
});
|
|
200
|
+
Object.entries(parameters.contractTxBaseGas).forEach(([tag, byAbiVersion]) => Object.entries(byAbiVersion !== null && byAbiVersion !== void 0 ? byAbiVersion : {}).forEach(([abiVersion, gas]) => {
|
|
201
|
+
var _Tag$tag2;
|
|
202
|
+
return checkUsableGas(`contractTxBaseGas.${(_Tag$tag2 = Tag[+tag]) !== null && _Tag$tag2 !== void 0 ? _Tag$tag2 : tag}.${abiVersion}`, gas);
|
|
203
|
+
}));
|
|
204
|
+
Object.entries(parameters.stateGasPerBlock).forEach(([tag, fraction]) => {
|
|
205
|
+
var _Tag$tag3, _fraction$whole;
|
|
206
|
+
const name = `stateGasPerBlock.${(_Tag$tag3 = Tag[+tag]) !== null && _Tag$tag3 !== void 0 ? _Tag$tag3 : tag}`;
|
|
207
|
+
checkUsableGas(`${name}.part`, fraction?.part);
|
|
208
|
+
// a `whole` of 0 makes the state gas — and so the fee — infinite
|
|
209
|
+
if (!Number.isSafeInteger(fraction?.whole) || ((_fraction$whole = fraction?.whole) !== null && _fraction$whole !== void 0 ? _fraction$whole : 0) < 1) {
|
|
210
|
+
throw new ArgumentError(`${name}.whole`, 'a positive safe integer', fraction?.whole);
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
usableParameters.add(parameters);
|
|
214
|
+
}
|
|
215
|
+
function checkRatioNotExcessive(name, {
|
|
216
|
+
part,
|
|
217
|
+
whole
|
|
218
|
+
}, sdkRatio) {
|
|
219
|
+
if (!Number.isSafeInteger(part) || !Number.isSafeInteger(whole)) {
|
|
220
|
+
throw malformedError(name, `${String(part)}/${String(whole)}`);
|
|
221
|
+
}
|
|
222
|
+
const limit = sdkRatio * maxRaiseFactor;
|
|
223
|
+
// a `whole` of 0 would make the fee infinite, `0 / 0` is NaN — both fail the check below
|
|
224
|
+
const ratio = part >= 0 && whole > 0 ? part / whole : NaN;
|
|
225
|
+
if (!(ratio >= 0) || ratio > limit) throw excessiveError(name, `${part}/${whole}`, limit);
|
|
226
|
+
return ratio / sdkRatio;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* The base gas the SDK release charges for a transaction type, whichever of the two tables it is
|
|
231
|
+
* in. `getTxBaseGas` reads `contractTxBaseGas` before `txBaseGas`, so an entry node adds to
|
|
232
|
+
* `contractTxBaseGas` overrides the `txBaseGas` of that very type — checking it against a
|
|
233
|
+
* type-independent constant would let node raise, say, a `PayingForTx` (3000) to 15000000 and
|
|
234
|
+
* still be counted as a raise of 1000.
|
|
235
|
+
*/
|
|
236
|
+
function defaultBaseGasOf(tag) {
|
|
237
|
+
var _d$contractTxBaseGas$;
|
|
238
|
+
const d = defaultProtocolParameters;
|
|
239
|
+
const values = [d.txBaseGas[tag], ...Object.values((_d$contractTxBaseGas$ = d.contractTxBaseGas[tag]) !== null && _d$contractTxBaseGas$ !== void 0 ? _d$contractTxBaseGas$ : {})].filter(value => value != null);
|
|
240
|
+
const max = values.length !== 0 ? maxOf(values) : BASE_GAS;
|
|
241
|
+
// a type charged 0 (`ChannelOffChainTx`) would get a budget of 0, and node reporting any gas for
|
|
242
|
+
// it would refuse the whole response. Give it an ordinary type's budget instead — a node charging
|
|
243
|
+
// more than `maxRaiseFactor` base gas units for it is still refused
|
|
244
|
+
return max !== 0 ? max : BASE_GAS;
|
|
245
|
+
}
|
|
246
|
+
function checkRaiseNotExcessive(what, raise) {
|
|
247
|
+
if (raise <= maxRaiseFactor) return;
|
|
248
|
+
throw new NodeError(`Node reports parameters raising ${what} ${Math.round(raise)} times above the one of the SDK` + ` release, more than the ${maxRaiseFactor} times the SDK considers plausible. Provide` + ' `protocolParameters` in options to build a transaction against these parameters anyway.');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Refuses the parameters if node reports values far above the ones of the SDK release, and returns
|
|
253
|
+
* the factors the two products a transaction costs may be raised by — see {@link ParameterRaises}.
|
|
254
|
+
*/
|
|
255
|
+
function checkParametersNotExcessive(parameters) {
|
|
256
|
+
const d = defaultProtocolParameters;
|
|
257
|
+
const defaultMinGasPrice = Number(d.minGasPrice);
|
|
258
|
+
// no network runs a consensus minimum of 0 (node reports 1e6 since Minerva), and with a miner
|
|
259
|
+
// minimum of 0 as well it makes every fee 0 and the `minFee / getFloorGasPrice` of the
|
|
260
|
+
// demand-based fee a division by zero
|
|
261
|
+
if (parameters.minGasPrice < 1n) {
|
|
262
|
+
throw new NodeError(`Node reports the minimum gas price as ${parameters.minGasPrice}, a transaction can't be` + ' priced against it. Provide `protocolParameters` in options to build a transaction' + ' against these parameters anyway.');
|
|
263
|
+
}
|
|
264
|
+
// both raise the gas price the fee is counted from: the consensus minimum directly, the miner
|
|
265
|
+
// minimum as the floor `getCachedIncreasedGasPrice` lifts the gas price to. Bounding the miner
|
|
266
|
+
// minimum on its own — as this used to — leaves node the product of the two to raise the fee by.
|
|
267
|
+
const gasPriceRaise = maxOf([checkNotExcessive('the minimum gas price', parameters.minGasPrice, defaultMinGasPrice), checkNotExcessive('the miner minimum gas price', parameters.minMinerGasPrice, defaultMinGasPrice)]);
|
|
268
|
+
|
|
269
|
+
// every one of these is a gas amount the fee is counted from, the biggest raise among them is
|
|
270
|
+
// the most a transaction's gas can grow by
|
|
271
|
+
const gasRaises = [checkNotExcessive('the gas per byte', parameters.gasPerByte, d.gasPerByte)];
|
|
272
|
+
Object.entries(parameters.txBaseGas).forEach(([tag, gas]) => {
|
|
273
|
+
// against the default of this very type and not against the biggest default of all types: a
|
|
274
|
+
// `ChannelForceProgressTx` costs 30 times a `SpendTx`, that is not a budget for a `SpendTx`
|
|
275
|
+
gasRaises.push(checkNotExcessive(`the base gas of ${Tag[+tag]}`, gas, defaultBaseGasOf(+tag)));
|
|
276
|
+
});
|
|
277
|
+
Object.entries(parameters.contractTxBaseGas).forEach(([tag, byAbiVersion]) => {
|
|
278
|
+
Object.entries(byAbiVersion).forEach(([abiVersion, gas]) => {
|
|
279
|
+
const name = `the base gas of ${Tag[+tag]} at abi version ${abiVersion}`;
|
|
280
|
+
// an abi version the SDK release prices is bounded by its own value — the budget of the
|
|
281
|
+
// priciest one is not a budget for the others. An abi version it doesn't price is charged
|
|
282
|
+
// the maximum of the ones it does, see `getTxBaseGas`
|
|
283
|
+
const sdkValue = d.contractTxBaseGas[+tag]?.[+abiVersion];
|
|
284
|
+
gasRaises.push(checkNotExcessive(name, gas, sdkValue !== null && sdkValue !== void 0 ? sdkValue : defaultBaseGasOf(+tag)));
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
const defaultStateGasRatio = defaultStateGasPerBlock.part / defaultStateGasPerBlock.whole;
|
|
288
|
+
Object.entries(parameters.stateGasPerBlock).forEach(([tag, fraction]) => {
|
|
289
|
+
const name = `the state gas per block of ${Tag[+tag]}`;
|
|
290
|
+
gasRaises.push(checkRatioNotExcessive(name, fraction, defaultStateGasRatio));
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// the minimum fee is `gasPrice * (baseGas + size * gasPerByte + stateGas)`, so each product is
|
|
294
|
+
// bounded rather than each parameter — otherwise node could combine them into a fee
|
|
295
|
+
// `maxRaiseFactor` squared above the one of the SDK release
|
|
296
|
+
const feeGasRaise = maxOf(gasRaises);
|
|
297
|
+
checkRaiseNotExcessive('the minimum transaction fee', gasPriceRaise * feeGasRaise);
|
|
298
|
+
|
|
299
|
+
// the two bound different transaction types, but both are a gas limit multiplied by the same
|
|
300
|
+
// gas price — the bigger of them is what the worst case costs
|
|
301
|
+
const blockGasLimitRaise = checkNotExcessive('the block gas limit', parameters.blockGasLimit, d.blockGasLimit);
|
|
302
|
+
const maxAuthFunGasRaise = checkNotExcessive('the max auth fun gas', parameters.maxAuthFunGas, d.maxAuthFunGas);
|
|
303
|
+
const gasLimitRaise = maxOf([blockGasLimitRaise, maxAuthFunGasRaise]);
|
|
304
|
+
checkRaiseNotExcessive('the cost of a contract transaction', gasPriceRaise * gasLimitRaise);
|
|
305
|
+
|
|
306
|
+
// the gas above is multiplied by a gas price that doesn't come from these parameters, and that
|
|
307
|
+
// is bounded on its own — so the raises are reported to lower those bounds by the same factors,
|
|
308
|
+
// see `getGasPriceDivisor` and `getGasLimitDivisor`
|
|
309
|
+
return {
|
|
310
|
+
fee: maxOf([1, feeGasRaise]),
|
|
311
|
+
blockGasLimit: maxOf([1, blockGasLimitRaise]),
|
|
312
|
+
maxAuthFunGas: maxOf([1, maxAuthFunGasRaise])
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Factor the gas price ceiling of `gas-price.ts` is lowered by, so that the worst case fee doesn't
|
|
318
|
+
* become the product of that ceiling and the fee gas bound of {@link convertProtocolParameters}.
|
|
319
|
+
* The gas limit raise is charged to the gas limit default instead ({@link getGasLimitDivisor}).
|
|
320
|
+
* It is 1 unless the parameters came from node.
|
|
321
|
+
*
|
|
322
|
+
* The biggest raise among all transaction types, not the one of the type being built — so a node
|
|
323
|
+
* repricing one type lowers the ceiling for every type. The ceiling is 100000 times the minimum
|
|
324
|
+
* gas price, far above the demand-based price this bounds in practice.
|
|
325
|
+
* @param parameters - Parameters a transaction is built against
|
|
326
|
+
*/
|
|
327
|
+
export function getGasPriceDivisor(parameters) {
|
|
328
|
+
var _parameterRaises$get$;
|
|
329
|
+
return (_parameterRaises$get$ = parameterRaises.get(parameters)?.fee) !== null && _parameterRaises$get$ !== void 0 ? _parameterRaises$get$ : 1;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* The cost of a contract transaction is `gasPrice * gasLimit`, and the gas limit the SDK defaults
|
|
334
|
+
* to comes from `blockGasLimit` — from `maxAuthFunGas` on a `GaMetaTx`. This is the factor node
|
|
335
|
+
* raises the one `tag` reads by, so that lowering the default by it keeps the product within the
|
|
336
|
+
* one of the SDK release. It is 1 unless the parameters came from node.
|
|
337
|
+
*
|
|
338
|
+
* Only the limit `tag` reads: charging the other one would shrink a gas limit because node raised
|
|
339
|
+
* something that transaction never reads. The fee gas raise is charged to the gas price ceiling
|
|
340
|
+
* ({@link getGasPriceDivisor}) instead.
|
|
341
|
+
* @param parameters - Parameters a transaction is built against
|
|
342
|
+
* @param tag - Type of the transaction the gas limit is defaulted for
|
|
343
|
+
*/
|
|
344
|
+
export function getGasLimitDivisor(parameters, tag) {
|
|
345
|
+
const raises = parameterRaises.get(parameters);
|
|
346
|
+
if (raises == null) return 1;
|
|
347
|
+
return tag === Tag.GaMetaTx ? raises.maxAuthFunGas : raises.blockGasLimit;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Gas price the minimum fee is counted at and the `gasPrice` of a contract transaction defaults
|
|
352
|
+
* to: the miner minimum when it is above the consensus one. Node reads the price a transaction
|
|
353
|
+
* pays as the lower of its `gasPrice` and its fee over its fee gas, so pricing either below the
|
|
354
|
+
* miner minimum gets it refused with `too_low_gas_price_for_miner`.
|
|
355
|
+
* @category transaction builder
|
|
356
|
+
* @param parameters - Parameters a transaction is built against
|
|
357
|
+
*/
|
|
358
|
+
export function getFloorGasPrice(parameters) {
|
|
359
|
+
const {
|
|
360
|
+
minGasPrice,
|
|
361
|
+
minMinerGasPrice
|
|
362
|
+
} = parameters;
|
|
363
|
+
return minMinerGasPrice > minGasPrice ? minMinerGasPrice : minGasPrice;
|
|
364
|
+
}
|
|
365
|
+
function mapByTxType(entries, map) {
|
|
366
|
+
return Object.fromEntries(entries.map(([txType, value]) => [getTag(txType), map(value)])
|
|
367
|
+
// a transaction type the SDK doesn't implement, or won't ever build (`ChannelClientReconnectTx`)
|
|
368
|
+
.filter(entry => entry[0] != null));
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Converts the node responses to the shape the transaction builder uses, rejecting values far
|
|
373
|
+
* above the ones the SDK was released with. Not a part of the public api, it is exported for
|
|
374
|
+
* tests — use {@link getCachedProtocolParameters}.
|
|
375
|
+
* @param response - Response of the `/v3/protocol-parameters` endpoint
|
|
376
|
+
* @param nodeSettings - Response of the `/v3/node-settings` endpoint
|
|
377
|
+
*/
|
|
378
|
+
export function convertProtocolParameters(response, nodeSettings) {
|
|
379
|
+
const {
|
|
380
|
+
currentProtocolVersion,
|
|
381
|
+
protocols
|
|
382
|
+
} = response;
|
|
383
|
+
const protocol = protocols.find(({
|
|
384
|
+
version
|
|
385
|
+
}) => version === currentProtocolVersion);
|
|
386
|
+
if (protocol == null) {
|
|
387
|
+
throw new InternalError(`Node doesn't provide parameters of the current protocol ${currentProtocolVersion}`);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// starts from the values of the SDK release so that a type or an abi version node doesn't report
|
|
391
|
+
// keeps building against them, as the SDK did before this endpoint existed, instead of failing
|
|
392
|
+
const contractTxBaseGas = Object.fromEntries(Object.entries(defaultProtocolParameters.contractTxBaseGas).map(([tag, byAbiVersion]) => [tag, {
|
|
393
|
+
...byAbiVersion
|
|
394
|
+
}]));
|
|
395
|
+
protocol.contractTxBaseGas.forEach(({
|
|
396
|
+
txType,
|
|
397
|
+
abiVersion,
|
|
398
|
+
txBaseGas
|
|
399
|
+
}) => {
|
|
400
|
+
var _contractTxBaseGas$ta;
|
|
401
|
+
const tag = getTag(txType);
|
|
402
|
+
// node controls `abiVersion` and the api client passes a value that doesn't match the schema
|
|
403
|
+
// node declares for it through unchanged. A `__proto__` key would be written into the
|
|
404
|
+
// prototype: invisible to the `Object.values` of the check below, and still returned by the
|
|
405
|
+
// `byAbiVersion[abiVersion]` lookup the fee is calculated from
|
|
406
|
+
if (tag == null || !Number.isInteger(abiVersion)) return;
|
|
407
|
+
const byAbiVersion = (_contractTxBaseGas$ta = contractTxBaseGas[tag]) !== null && _contractTxBaseGas$ta !== void 0 ? _contractTxBaseGas$ta : contractTxBaseGas[tag] = {};
|
|
408
|
+
byAbiVersion[abiVersion] = txBaseGas;
|
|
409
|
+
});
|
|
410
|
+
const reportedTxBaseGas = mapByTxType(Object.entries(protocol.txBaseGas), gas => gas);
|
|
411
|
+
const parameters = freezeParameters({
|
|
412
|
+
minGasPrice: protocol.minimumGasPrice,
|
|
413
|
+
minMinerGasPrice: nodeSettings.minMinerGasPrice,
|
|
414
|
+
gasPerByte: protocol.gasPerByte,
|
|
415
|
+
blockGasLimit: nodeSettings.blockGasLimit,
|
|
416
|
+
maxAuthFunGas: nodeSettings.maxAuthFunGas,
|
|
417
|
+
txBaseGas: {
|
|
418
|
+
// see the note on `contractTxBaseGas` above
|
|
419
|
+
...defaultProtocolParameters.txBaseGas,
|
|
420
|
+
...reportedTxBaseGas
|
|
421
|
+
},
|
|
422
|
+
contractTxBaseGas,
|
|
423
|
+
stateGasPerBlock: {
|
|
424
|
+
// see the note on `contractTxBaseGas` above
|
|
425
|
+
...defaultProtocolParameters.stateGasPerBlock,
|
|
426
|
+
...mapByTxType(Object.entries(protocol.stateGasPerBlock), fraction => ({
|
|
427
|
+
part: fraction.part,
|
|
428
|
+
whole: fraction.whole
|
|
429
|
+
}))
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
parameterRaises.set(parameters, checkParametersNotExcessive(parameters));
|
|
433
|
+
return parameters;
|
|
434
|
+
}
|
|
435
|
+
const cache = new WeakMap();
|
|
436
|
+
// protocol parameters change on a hard fork, or when the node operator edits the node config,
|
|
437
|
+
// neither happens often enough to justify a request per transaction built
|
|
438
|
+
const cacheTtl = 10 * 60 * 1000;
|
|
439
|
+
// a node that can't be reached, or that reports parameters the SDK refuses to build against, may
|
|
440
|
+
// be reachable or reconfigured in a moment — don't keep the whole `cacheTtl` worth of transactions
|
|
441
|
+
// building against the parameters of the SDK release, or failing, because of one bad response.
|
|
442
|
+
// Still cached rather than retried per transaction: both outcomes cost a round trip to reproduce.
|
|
443
|
+
const transientCacheTtl = 30 * 1000;
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Requests protocol parameters from node, the result is cached per node instance.
|
|
447
|
+
*
|
|
448
|
+
* Falls back to {@link defaultProtocolParameters} for a node that doesn't provide the endpoints,
|
|
449
|
+
* can't be reached, or answers something this SDK release can't read. Rejects only when node
|
|
450
|
+
* reports parameters that would price a transaction far above the SDK release — see
|
|
451
|
+
* {@link ProtocolParametersOption} for the way out of that.
|
|
452
|
+
*
|
|
453
|
+
* The cache is keyed by the `Node` object, so code that constructs a `Node` per request (a
|
|
454
|
+
* serverless handler, an edge worker) requests the parameters again for each one. Keep one `Node`
|
|
455
|
+
* instance per endpoint to get the benefit of the cache.
|
|
456
|
+
* @category transaction builder
|
|
457
|
+
* @param nodeOrProxy - Node to request the parameters from
|
|
458
|
+
*/
|
|
459
|
+
export async function getCachedProtocolParameters(nodeOrProxy) {
|
|
460
|
+
const node = unwrapProxy(nodeOrProxy);
|
|
461
|
+
const entry = cache.get(node);
|
|
462
|
+
if (entry != null && entry.expiresAt > Date.now()) return entry.parameters;
|
|
463
|
+
|
|
464
|
+
// node is expected to respond with 404 until it gets the endpoints, and a node whose operator
|
|
465
|
+
// disabled the `node_info`/`node_settings` endpoint groups with 403 — neither changes within
|
|
466
|
+
// `cacheTtl`, while anything else is worth asking again about sooner
|
|
467
|
+
let ttl = cacheTtl;
|
|
468
|
+
// the request and the conversion both run in here so that only a deliberate refusal — a
|
|
469
|
+
// `NodeError` on implausible parameters — comes out as a rejection; anything unreadable falls
|
|
470
|
+
// back to the parameters of the SDK release instead of making every transaction unbuildable
|
|
471
|
+
const parameters = (async () => {
|
|
472
|
+
const fallBack = reason => {
|
|
473
|
+
ttl = transientCacheTtl;
|
|
474
|
+
console.warn("Can't get protocol parameters from node, using the ones of the SDK release instead." + ' A transaction may be rejected if this node runs other parameters.' + ` Reason: ${reason instanceof Error ? reason.message : String(reason)}`);
|
|
475
|
+
return defaultProtocolParameters;
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
// a node object without the methods (a test double, another SDK copy's `Node`) won't grow
|
|
479
|
+
// them within `transientCacheTtl` — treat like the 404 of an old node: full-interval cache,
|
|
480
|
+
// and no warning per 30 seconds for the life of the process
|
|
481
|
+
if (typeof node.getProtocolParameters !== 'function' || typeof node.getNodeSettings !== 'function') {
|
|
482
|
+
return defaultProtocolParameters;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// retrying the 404 on every transaction built would only add latency
|
|
486
|
+
const noRetry = {
|
|
487
|
+
requestOptions: {
|
|
488
|
+
customHeaders: {
|
|
489
|
+
'__no-retry': 'true'
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
let response;
|
|
494
|
+
let nodeSettings;
|
|
495
|
+
try {
|
|
496
|
+
// consensus parameters and this node's own policy are two endpoints, and a transaction is
|
|
497
|
+
// priced by both — requested together so that a build waits for one round trip and not two,
|
|
498
|
+
// and taken as a set: parameters half from node and half from the SDK release would be
|
|
499
|
+
// neither ones node accepts nor ones the SDK was tested against
|
|
500
|
+
[response, nodeSettings] = await Promise.all([node.getProtocolParameters(noRetry), node.getNodeSettings(noRetry)]);
|
|
501
|
+
} catch (error) {
|
|
502
|
+
// the 404/403 of the note above — neither is worth a warning, or an earlier retry.
|
|
503
|
+
// Read off the error rather than through `instanceof RestError`: that would import
|
|
504
|
+
// `@azure/core-rest-pipeline` as a value into the transaction builder, which is otherwise
|
|
505
|
+
// free of it, and pull the http stack into the bundle of an application that only packs
|
|
506
|
+
// transactions. The api client is the only thing that rejects here, and it sets this
|
|
507
|
+
const {
|
|
508
|
+
statusCode
|
|
509
|
+
} = error;
|
|
510
|
+
if (statusCode === 404 || statusCode === 403) return defaultProtocolParameters;
|
|
511
|
+
// node is syncing (503), a proxy in between is down, the request timed out
|
|
512
|
+
return fallBack(error);
|
|
513
|
+
}
|
|
514
|
+
try {
|
|
515
|
+
return convertProtocolParameters(response, nodeSettings);
|
|
516
|
+
} catch (error) {
|
|
517
|
+
// node reports parameters that would price a transaction far above the SDK release — falling
|
|
518
|
+
// back would build a transaction this node rejects, so the caller is told to decide instead
|
|
519
|
+
if (error instanceof NodeError) throw error;
|
|
520
|
+
return fallBack(error);
|
|
521
|
+
}
|
|
522
|
+
})();
|
|
523
|
+
|
|
524
|
+
// until it settles the entry keeps concurrent builds on this one request
|
|
525
|
+
const cacheEntry = {
|
|
526
|
+
expiresAt: Date.now() + cacheTtl,
|
|
527
|
+
parameters
|
|
528
|
+
};
|
|
529
|
+
parameters.then(() => {
|
|
530
|
+
cacheEntry.expiresAt = Date.now() + ttl;
|
|
531
|
+
},
|
|
532
|
+
// see `transientCacheTtl`
|
|
533
|
+
() => {
|
|
534
|
+
cacheEntry.expiresAt = Date.now() + transientCacheTtl;
|
|
535
|
+
});
|
|
536
|
+
cache.set(node, cacheEntry);
|
|
537
|
+
return parameters;
|
|
538
|
+
}
|
|
539
|
+
//# sourceMappingURL=protocol-parameters.js.map
|