@vultisig/core-chain 2.35.0 → 2.35.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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @vultisig/core-chain
2
2
 
3
+ ## 2.35.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#1846](https://github.com/vultisig/vultisig-sdk/pull/1846) [`8faa612`](https://github.com/vultisig/vultisig-sdk/commit/8faa612e6358af0a77cebf9c06d8865d832b69df) Thanks [@Ehsan-saradar](https://github.com/Ehsan-saradar)! - Reserve the OP-stack balance-check surcharges in the EVM fee amount, so a native max send on Optimism, Base, Blast or Mantle stays affordable at broadcast. op-geth requires `value + gasLimit * maxFeePerGas + l1Cost + operatorCost`, and an amount that left only the gas term behind was rejected by exactly the difference — after the keysign ceremony had already run. Both oracle reads fail open, so a chain whose oracle is unreachable or predates a term behaves exactly as before.
8
+
3
9
  ## 2.35.0
4
10
 
5
11
  ### Minor Changes
@@ -0,0 +1,22 @@
1
+ import { OpStackChain } from './opStackChains.js';
2
+ type GetOpStackFeeSurchargeInput = {
3
+ chain: OpStackChain;
4
+ gasLimit: bigint;
5
+ callDataSize: number;
6
+ };
7
+ /**
8
+ * The two terms op-geth adds to the balance check it runs before executing a
9
+ * transaction: the L1 data-availability cost and, from Isthmus onwards, the
10
+ * operator fee. The chain requires
11
+ * `value + gasLimit * maxFeePerGas + l1Cost + operatorCost` to be covered, so an
12
+ * amount that leaves only the gas term behind is rejected by exactly this much.
13
+ *
14
+ * Both terms fail open independently. A pre-Isthmus oracle reverts on
15
+ * `getOperatorFee`, and letting that failure take the L1 data fee down with it
16
+ * would leave the chain reserving nothing at all — a fee that cannot be read
17
+ * must never block a send that worked before. A negative answer is nonsense from
18
+ * a fee oracle and is floored rather than passed on to widen a max amount.
19
+ */
20
+ export declare const getOpStackFeeSurcharge: ({ chain, gasLimit, callDataSize, }: GetOpStackFeeSurchargeInput) => Promise<bigint>;
21
+ export {};
22
+ //# sourceMappingURL=getOpStackFeeSurcharge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"getOpStackFeeSurcharge.d.ts","sourceRoot":"","sources":["../../../../../../../packages/core/chain/chains/evm/opStack/getOpStackFeeSurcharge.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAoC9C,KAAK,2BAA2B,GAAG;IACjC,KAAK,EAAE,YAAY,CAAA;IACnB,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,EAAE,MAAM,CAAA;CACrB,CAAA;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,sBAAsB,GAAU,oCAI1C,2BAA2B,KAAG,OAAO,CAAC,MAAM,CAiC9C,CAAA"}
@@ -0,0 +1,68 @@
1
+ import { getEvmClient } from '@vultisig/core-chain/chains/evm/client';
2
+ import { attempt, withFallback } from '@vultisig/lib-utils/attempt';
3
+ import { bigIntMax } from '@vultisig/lib-utils/bigint/bigIntMax';
4
+ import { l1FeeProbeData } from './l1FeeProbeData.js';
5
+ /** Canonical `GasPriceOracle` predeploy address, identical on every OP-stack rollup. */
6
+ const gasPriceOracleAddress = '0x420000000000000000000000000000000000000F';
7
+ // `getL1Fee(bytes)` is preferred over the purpose-built `getL1FeeUpperBound(uint256)`,
8
+ // which only exists from Fjord onwards — Blast's oracle predates it, while
9
+ // `getL1Fee` has been there since Bedrock. `getOperatorFee(uint256)` arrives with
10
+ // Isthmus and simply reverts on older oracles, which reads the same as "this chain
11
+ // charges no operator fee".
12
+ const gasPriceOracleAbi = [
13
+ {
14
+ name: 'getL1Fee',
15
+ type: 'function',
16
+ stateMutability: 'view',
17
+ inputs: [{ name: '_data', type: 'bytes' }],
18
+ outputs: [{ name: '', type: 'uint256' }],
19
+ },
20
+ {
21
+ name: 'getOperatorFee',
22
+ type: 'function',
23
+ stateMutability: 'view',
24
+ inputs: [{ name: '_gasUsed', type: 'uint256' }],
25
+ outputs: [{ name: '', type: 'uint256' }],
26
+ },
27
+ ];
28
+ // Serialized size, in bytes, that the L1-fee probe stands in for on top of the
29
+ // transaction's own calldata. The oracle wants the UNSIGNED transaction and adds
30
+ // its own 68-byte allowance for the signature, so this models the ~70-byte
31
+ // unsigned EIP-1559 envelope with deliberate margin: the transaction is not built
32
+ // yet when its fee has to be reserved, and over-reserving leaves dust behind
33
+ // where under-reserving costs a broadcast rejected after the keysign ceremony has
34
+ // already run.
35
+ const unsignedTxEnvelopeSize = 160;
36
+ /**
37
+ * The two terms op-geth adds to the balance check it runs before executing a
38
+ * transaction: the L1 data-availability cost and, from Isthmus onwards, the
39
+ * operator fee. The chain requires
40
+ * `value + gasLimit * maxFeePerGas + l1Cost + operatorCost` to be covered, so an
41
+ * amount that leaves only the gas term behind is rejected by exactly this much.
42
+ *
43
+ * Both terms fail open independently. A pre-Isthmus oracle reverts on
44
+ * `getOperatorFee`, and letting that failure take the L1 data fee down with it
45
+ * would leave the chain reserving nothing at all — a fee that cannot be read
46
+ * must never block a send that worked before. A negative answer is nonsense from
47
+ * a fee oracle and is floored rather than passed on to widen a max amount.
48
+ */
49
+ export const getOpStackFeeSurcharge = async ({ chain, gasLimit, callDataSize, }) => {
50
+ const client = getEvmClient(chain);
51
+ const l1DataFee = withFallback(attempt(client.readContract({
52
+ address: gasPriceOracleAddress,
53
+ abi: gasPriceOracleAbi,
54
+ functionName: 'getL1Fee',
55
+ args: [l1FeeProbeData(unsignedTxEnvelopeSize + callDataSize)],
56
+ })), 0n);
57
+ const operatorFee = gasLimit > 0n
58
+ ? withFallback(attempt(client.readContract({
59
+ address: gasPriceOracleAddress,
60
+ abi: gasPriceOracleAbi,
61
+ functionName: 'getOperatorFee',
62
+ args: [gasLimit],
63
+ })), 0n)
64
+ : Promise.resolve(0n);
65
+ const terms = await Promise.all([l1DataFee, operatorFee]);
66
+ return terms.reduce((total, term) => total + bigIntMax(0n, term), 0n);
67
+ };
68
+ //# sourceMappingURL=getOpStackFeeSurcharge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"getOpStackFeeSurcharge.js","sourceRoot":"","sources":["../../../../../../../packages/core/chain/chains/evm/opStack/getOpStackFeeSurcharge.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,wCAAwC,CAAA;AACrE,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAA;AACnE,OAAO,EAAE,SAAS,EAAE,MAAM,sCAAsC,CAAA;AAEhE,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AAGjD,wFAAwF;AACxF,MAAM,qBAAqB,GAAG,4CAA4C,CAAA;AAE1E,uFAAuF;AACvF,2EAA2E;AAC3E,kFAAkF;AAClF,mFAAmF;AACnF,4BAA4B;AAC5B,MAAM,iBAAiB,GAAG;IACxB;QACE,IAAI,EAAE,UAAU;QAChB,IAAI,EAAE,UAAU;QAChB,eAAe,EAAE,MAAM;QACvB,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;QAC1C,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;KACzC;IACD;QACE,IAAI,EAAE,gBAAgB;QACtB,IAAI,EAAE,UAAU;QAChB,eAAe,EAAE,MAAM;QACvB,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;QAC/C,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;KACzC;CACO,CAAA;AAEV,+EAA+E;AAC/E,iFAAiF;AACjF,2EAA2E;AAC3E,kFAAkF;AAClF,6EAA6E;AAC7E,kFAAkF;AAClF,eAAe;AACf,MAAM,sBAAsB,GAAG,GAAG,CAAA;AAQlC;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,KAAK,EAAE,EAC3C,KAAK,EACL,QAAQ,EACR,YAAY,GACgB,EAAmB,EAAE;IACjD,MAAM,MAAM,GAAG,YAAY,CAAC,KAAK,CAAC,CAAA;IAElC,MAAM,SAAS,GAAG,YAAY,CAC5B,OAAO,CACL,MAAM,CAAC,YAAY,CAAC;QAClB,OAAO,EAAE,qBAAqB;QAC9B,GAAG,EAAE,iBAAiB;QACtB,YAAY,EAAE,UAAU;QACxB,IAAI,EAAE,CAAC,cAAc,CAAC,sBAAsB,GAAG,YAAY,CAAC,CAAC;KAC9D,CAAC,CACH,EACD,EAAE,CACH,CAAA;IAED,MAAM,WAAW,GACf,QAAQ,GAAG,EAAE;QACX,CAAC,CAAC,YAAY,CACV,OAAO,CACL,MAAM,CAAC,YAAY,CAAC;YAClB,OAAO,EAAE,qBAAqB;YAC9B,GAAG,EAAE,iBAAiB;YACtB,YAAY,EAAE,gBAAgB;YAC9B,IAAI,EAAE,CAAC,QAAQ,CAAC;SACjB,CAAC,CACH,EACD,EAAE,CACH;QACH,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;IAEzB,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC,CAAA;IAEzD,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,KAAK,GAAG,SAAS,CAAC,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAAA;AACvE,CAAC,CAAA"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Stand-in bytes for the serialized transaction handed to the `GasPriceOracle`
3
+ * predeploy's `getL1Fee(bytes)`, which has to price a transaction that does not
4
+ * exist yet — the amount being priced is itself derived from the answer.
5
+ *
6
+ * The bytes are deliberately incompressible. From Fjord onwards the oracle
7
+ * charges for the FastLZ-compressed size of what it is handed, so a *periodic*
8
+ * payload prices far below what a real transaction costs: a plain counter wraps
9
+ * every 256 bytes and every window after the first lap is a back reference, at
10
+ * which point the quote stops growing with size at all.
11
+ *
12
+ * This stream repeats no 3-byte window — FastLZ's shortest encodable match —
13
+ * through its first 3,652 bytes, which covers every probe the callers build
14
+ * (a 160-byte envelope plus calldata, the largest being a ~3 KB swap router
15
+ * call). Past that the birthday bound makes isolated repeats inevitable in any
16
+ * uniformly distributed stream, and they are harmless: a lone 3-byte match
17
+ * costs more to encode as a back reference than it saves, so FastLZ does not
18
+ * take it. Measured against the live oracles, this stream prices identically to
19
+ * crypto-random bytes at 1–4 KB and 0.01% *above* them at 8–16 KB — it never
20
+ * quotes below the incompressible ideal.
21
+ */
22
+ export declare const l1FeeProbeData: (size: number) => `0x${string}`;
23
+ //# sourceMappingURL=l1FeeProbeData.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"l1FeeProbeData.d.ts","sourceRoot":"","sources":["../../../../../../../packages/core/chain/chains/evm/opStack/l1FeeProbeData.ts"],"names":[],"mappings":"AASA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,cAAc,GAAI,MAAM,MAAM,KAAG,KAAK,MAAM,EAUxD,CAAA"}
@@ -0,0 +1,38 @@
1
+ import { bytesToHex } from 'viem';
2
+ // Linear congruential generator (Numerical Recipes constants) seeded with the
3
+ // golden-ratio word. Deterministic across runs, so the same send reserves the
4
+ // same amount twice running, and aperiodic over any length we ask it for.
5
+ const lcgSeed = 0x9e3779b9;
6
+ const lcgMultiplier = 1_664_525;
7
+ const lcgIncrement = 1_013_904_223;
8
+ /**
9
+ * Stand-in bytes for the serialized transaction handed to the `GasPriceOracle`
10
+ * predeploy's `getL1Fee(bytes)`, which has to price a transaction that does not
11
+ * exist yet — the amount being priced is itself derived from the answer.
12
+ *
13
+ * The bytes are deliberately incompressible. From Fjord onwards the oracle
14
+ * charges for the FastLZ-compressed size of what it is handed, so a *periodic*
15
+ * payload prices far below what a real transaction costs: a plain counter wraps
16
+ * every 256 bytes and every window after the first lap is a back reference, at
17
+ * which point the quote stops growing with size at all.
18
+ *
19
+ * This stream repeats no 3-byte window — FastLZ's shortest encodable match —
20
+ * through its first 3,652 bytes, which covers every probe the callers build
21
+ * (a 160-byte envelope plus calldata, the largest being a ~3 KB swap router
22
+ * call). Past that the birthday bound makes isolated repeats inevitable in any
23
+ * uniformly distributed stream, and they are harmless: a lone 3-byte match
24
+ * costs more to encode as a back reference than it saves, so FastLZ does not
25
+ * take it. Measured against the live oracles, this stream prices identically to
26
+ * crypto-random bytes at 1–4 KB and 0.01% *above* them at 8–16 KB — it never
27
+ * quotes below the incompressible ideal.
28
+ */
29
+ export const l1FeeProbeData = (size) => {
30
+ const bytes = new Uint8Array(Math.max(0, size));
31
+ let state = lcgSeed;
32
+ for (let i = 0; i < bytes.length; i++) {
33
+ state = (Math.imul(state, lcgMultiplier) + lcgIncrement) >>> 0;
34
+ bytes[i] = (state >>> 24) & 0xff;
35
+ }
36
+ return bytesToHex(bytes);
37
+ };
38
+ //# sourceMappingURL=l1FeeProbeData.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"l1FeeProbeData.js","sourceRoot":"","sources":["../../../../../../../packages/core/chain/chains/evm/opStack/l1FeeProbeData.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,MAAM,CAAA;AAEjC,8EAA8E;AAC9E,8EAA8E;AAC9E,0EAA0E;AAC1E,MAAM,OAAO,GAAG,UAAU,CAAA;AAC1B,MAAM,aAAa,GAAG,SAAS,CAAA;AAC/B,MAAM,YAAY,GAAG,aAAa,CAAA;AAElC;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,IAAY,EAAiB,EAAE;IAC5D,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAA;IAE/C,IAAI,KAAK,GAAG,OAAO,CAAA;IACnB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,KAAK,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,aAAa,CAAC,GAAG,YAAY,CAAC,KAAK,CAAC,CAAA;QAC9D,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,KAAK,EAAE,CAAC,GAAG,IAAI,CAAA;IAClC,CAAC;IAED,OAAO,UAAU,CAAC,KAAK,CAAC,CAAA;AAC1B,CAAC,CAAA"}
@@ -0,0 +1,19 @@
1
+ import { Chain } from '@vultisig/core-chain/Chain';
2
+ /**
3
+ * EVM chains that settle to Ethereum through the OP stack and therefore expose
4
+ * the `GasPriceOracle` predeploy. op-geth bills these chains' transactions an
5
+ * L1 data-availability cost — and, from Isthmus onwards, an operator fee — on
6
+ * top of L2 execution gas, and adds both to the balance check it runs before a
7
+ * transaction executes.
8
+ *
9
+ * Membership is asserted, not detected: the predeploy answers on exactly these
10
+ * four and has no code on every other EVM chain in the registry. An omission is
11
+ * safe but silent — the chain reserves nothing extra, which is how every chain
12
+ * behaved before the oracle was read at all — so a newly added OP-stack rollup
13
+ * has to be listed here or its max sends will under-reserve.
14
+ */
15
+ export declare const opStackChains: readonly ["Optimism", "Base", "Blast", "Mantle"];
16
+ export type OpStackChain = (typeof opStackChains)[number];
17
+ /** Narrows a chain to one whose node prices an L1 data fee through the `GasPriceOracle` predeploy. */
18
+ export declare const isOpStackChain: (chain: Chain) => chain is OpStackChain;
19
+ //# sourceMappingURL=opStackChains.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"opStackChains.d.ts","sourceRoot":"","sources":["../../../../../../../packages/core/chain/chains/evm/opStack/opStackChains.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAY,MAAM,4BAA4B,CAAA;AAG5D;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,aAAa,kDAA+E,CAAA;AAEzG,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,CAAA;AAEzD,sGAAsG;AACtG,eAAO,MAAM,cAAc,GAAI,OAAO,KAAK,KAAG,KAAK,IAAI,YAA6C,CAAA"}
@@ -0,0 +1,19 @@
1
+ import { EvmChain } from '@vultisig/core-chain/Chain';
2
+ import { isOneOf } from '@vultisig/lib-utils/array/isOneOf';
3
+ /**
4
+ * EVM chains that settle to Ethereum through the OP stack and therefore expose
5
+ * the `GasPriceOracle` predeploy. op-geth bills these chains' transactions an
6
+ * L1 data-availability cost — and, from Isthmus onwards, an operator fee — on
7
+ * top of L2 execution gas, and adds both to the balance check it runs before a
8
+ * transaction executes.
9
+ *
10
+ * Membership is asserted, not detected: the predeploy answers on exactly these
11
+ * four and has no code on every other EVM chain in the registry. An omission is
12
+ * safe but silent — the chain reserves nothing extra, which is how every chain
13
+ * behaved before the oracle was read at all — so a newly added OP-stack rollup
14
+ * has to be listed here or its max sends will under-reserve.
15
+ */
16
+ export const opStackChains = [EvmChain.Optimism, EvmChain.Base, EvmChain.Blast, EvmChain.Mantle];
17
+ /** Narrows a chain to one whose node prices an L1 data fee through the `GasPriceOracle` predeploy. */
18
+ export const isOpStackChain = (chain) => isOneOf(chain, opStackChains);
19
+ //# sourceMappingURL=opStackChains.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"opStackChains.js","sourceRoot":"","sources":["../../../../../../../packages/core/chain/chains/evm/opStack/opStackChains.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,QAAQ,EAAE,MAAM,4BAA4B,CAAA;AAC5D,OAAO,EAAE,OAAO,EAAE,MAAM,mCAAmC,CAAA;AAE3D;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAU,CAAA;AAIzG,sGAAsG;AACtG,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,KAAY,EAAyB,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,aAAa,CAAC,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vultisig/core-chain",
3
- "version": "2.35.0",
3
+ "version": "2.35.1",
4
4
  "description": "Blockchain chain logic shared across Vultisig clients",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -640,6 +640,21 @@
640
640
  "import": "./dist/chains/evm/noon/index.js",
641
641
  "default": "./dist/chains/evm/noon/index.js"
642
642
  },
643
+ "./chains/evm/opStack/getOpStackFeeSurcharge": {
644
+ "types": "./dist/chains/evm/opStack/getOpStackFeeSurcharge.d.ts",
645
+ "import": "./dist/chains/evm/opStack/getOpStackFeeSurcharge.js",
646
+ "default": "./dist/chains/evm/opStack/getOpStackFeeSurcharge.js"
647
+ },
648
+ "./chains/evm/opStack/l1FeeProbeData": {
649
+ "types": "./dist/chains/evm/opStack/l1FeeProbeData.d.ts",
650
+ "import": "./dist/chains/evm/opStack/l1FeeProbeData.js",
651
+ "default": "./dist/chains/evm/opStack/l1FeeProbeData.js"
652
+ },
653
+ "./chains/evm/opStack/opStackChains": {
654
+ "types": "./dist/chains/evm/opStack/opStackChains.d.ts",
655
+ "import": "./dist/chains/evm/opStack/opStackChains.js",
656
+ "default": "./dist/chains/evm/opStack/opStackChains.js"
657
+ },
643
658
  "./chains/evm/tx/fee": {
644
659
  "types": "./dist/chains/evm/tx/fee/index.d.ts",
645
660
  "import": "./dist/chains/evm/tx/fee/index.js",