ox 1.8.12 → 1.8.13

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 (55) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/core/TxEnvelope.d.ts.map +1 -1
  3. package/dist/core/TxEnvelope.js +5 -3
  4. package/dist/core/TxEnvelope.js.map +1 -1
  5. package/dist/core/TxEnvelopeEip1559.d.ts.map +1 -1
  6. package/dist/core/TxEnvelopeEip1559.js +6 -2
  7. package/dist/core/TxEnvelopeEip1559.js.map +1 -1
  8. package/dist/core/TxEnvelopeEip4844.d.ts.map +1 -1
  9. package/dist/core/TxEnvelopeEip4844.js +4 -0
  10. package/dist/core/TxEnvelopeEip4844.js.map +1 -1
  11. package/dist/core/TxEnvelopeEip7702.d.ts.map +1 -1
  12. package/dist/core/TxEnvelopeEip7702.js +4 -0
  13. package/dist/core/TxEnvelopeEip7702.js.map +1 -1
  14. package/dist/core/TypedData.d.ts +8 -1
  15. package/dist/core/TypedData.d.ts.map +1 -1
  16. package/dist/core/TypedData.js +51 -14
  17. package/dist/core/TypedData.js.map +1 -1
  18. package/dist/core/internal/abiItem.d.ts.map +1 -1
  19. package/dist/core/internal/abiItem.js +44 -13
  20. package/dist/core/internal/abiItem.js.map +1 -1
  21. package/dist/core/internal/abiParameters.d.ts +2 -2
  22. package/dist/core/internal/abiParameters.d.ts.map +1 -1
  23. package/dist/core/internal/abiParameters.js +24 -7
  24. package/dist/core/internal/abiParameters.js.map +1 -1
  25. package/dist/core/internal/rpcSchemas/eth.d.ts +3 -2
  26. package/dist/core/internal/rpcSchemas/eth.d.ts.map +1 -1
  27. package/dist/tempo/KeyAuthorization.d.ts +21 -6
  28. package/dist/tempo/KeyAuthorization.d.ts.map +1 -1
  29. package/dist/tempo/KeyAuthorization.js +7 -4
  30. package/dist/tempo/KeyAuthorization.js.map +1 -1
  31. package/dist/tempo/TxEnvelopeTempo.d.ts.map +1 -1
  32. package/dist/tempo/TxEnvelopeTempo.js +6 -2
  33. package/dist/tempo/TxEnvelopeTempo.js.map +1 -1
  34. package/package.json +5 -2
  35. package/src/core/TxEnvelope.ts +5 -3
  36. package/src/core/TxEnvelopeEip1559.ts +8 -2
  37. package/src/core/TxEnvelopeEip4844.ts +6 -0
  38. package/src/core/TxEnvelopeEip7702.ts +6 -0
  39. package/src/core/TypedData.ts +57 -13
  40. package/src/core/_test/AbiItem.test.ts +238 -4
  41. package/src/core/_test/AbiParameters.decode.test.ts +140 -1
  42. package/src/core/_test/RpcSchema.test-d.ts +23 -1
  43. package/src/core/_test/TransactionEnvelopeEip1559.test.ts +24 -0
  44. package/src/core/_test/TransactionEnvelopeEip4844.test.ts +13 -0
  45. package/src/core/_test/TransactionEnvelopeEip7702.test.ts +13 -0
  46. package/src/core/_test/TypedData.test.ts +188 -1
  47. package/src/core/internal/abiItem.ts +48 -17
  48. package/src/core/internal/abiParameters.ts +26 -8
  49. package/src/core/internal/rpcSchemas/eth.ts +2 -1
  50. package/src/tempo/KeyAuthorization.test.ts +6 -4
  51. package/src/tempo/KeyAuthorization.ts +26 -8
  52. package/src/tempo/TxEnvelopeTempo.test.ts +26 -0
  53. package/src/tempo/TxEnvelopeTempo.ts +8 -2
  54. package/src/tempo/e2e.test.ts +73 -17
  55. package/src/version.ts +1 -1
@@ -425,7 +425,7 @@ describe('assert', () => {
425
425
  ).toThrowErrorMatchingInlineSnapshot(`
426
426
  [TypedData.InvalidStructTypeError: Struct type "address" is invalid.
427
427
 
428
- Struct type must not be a Solidity type.]
428
+ Struct type must contain only letters, digits, "_" and "$", and must not be a Solidity type.]
429
429
  `)
430
430
  })
431
431
 
@@ -498,6 +498,161 @@ describe('assert', () => {
498
498
  [Hex.IntegerOutOfRangeError: Number \`256n\` is not in safe 8-bit unsigned integer range (\`0n\` to \`255n\`)]
499
499
  `)
500
500
  })
501
+ test.each([
502
+ '1Order',
503
+ '_Order',
504
+ 'bytesInfo',
505
+ 'Foo$Bar',
506
+ 'intent',
507
+ 'interval',
508
+ 'uintPair',
509
+ ])('struct type: %s', (type) => {
510
+ TypedData.assert({
511
+ types: {
512
+ Order: [{ name: 'inner', type }],
513
+ [type]: [{ name: 'amount', type: 'uint256' }],
514
+ },
515
+ primaryType: 'Order',
516
+ message: { inner: { amount: 1n } },
517
+ } as any)
518
+ })
519
+
520
+ test.each([
521
+ 'address',
522
+ 'bool',
523
+ 'bytes',
524
+ 'bytes32',
525
+ 'bytes32[]',
526
+ 'bytes33',
527
+ 'Foo.Bar',
528
+ 'int',
529
+ 'int8',
530
+ 'int8[2]',
531
+ 'string',
532
+ 'uint',
533
+ 'uint256',
534
+ 'uint256 ',
535
+ 'uint256[]',
536
+ 'uint7',
537
+ ])('invalid struct type: %s', (type) => {
538
+ expect(() =>
539
+ TypedData.assert({
540
+ types: {
541
+ Order: [{ name: 'inner', type }],
542
+ [type]: [{ name: 'amount', type: 'uint256' }],
543
+ },
544
+ primaryType: 'Order',
545
+ message: { inner: { amount: 1n } },
546
+ } as any),
547
+ ).toThrowError(`Struct type "${type}" is invalid.`)
548
+ })
549
+
550
+ test('invalid struct type: array element', () => {
551
+ expect(() =>
552
+ TypedData.assert({
553
+ types: {
554
+ Message: [{ name: 'contents', type: 'bytes32[]' }],
555
+ bytes32: [],
556
+ },
557
+ primaryType: 'Message',
558
+ message: { contents: [Hex.padLeft('0x11', 32)] },
559
+ } as any),
560
+ ).toThrowErrorMatchingInlineSnapshot(`
561
+ [TypedData.InvalidStructTypeError: Struct type "bytes32" is invalid.
562
+
563
+ Struct type must contain only letters, digits, "_" and "$", and must not be a Solidity type.]
564
+ `)
565
+ })
566
+
567
+ test.each([
568
+ 'bytes0',
569
+ 'bytes33',
570
+ 'int0',
571
+ 'int7',
572
+ 'uint08',
573
+ 'uint7',
574
+ 'uint7[]',
575
+ 'uint264',
576
+ 'uint264[2]',
577
+ ])('invalid type: %s', (type) => {
578
+ expect(() =>
579
+ TypedData.assert({
580
+ types: {
581
+ Message: [{ name: 'amount', type }],
582
+ },
583
+ primaryType: 'Message',
584
+ message: {
585
+ amount: type.includes('[')
586
+ ? [1n, 2n]
587
+ : type.startsWith('bytes')
588
+ ? '0x'
589
+ : 1n,
590
+ },
591
+ } as any),
592
+ ).toThrowError(`Type "${type}" is not a valid EIP-712 type.`)
593
+ })
594
+
595
+ test('invalid type: alias', () => {
596
+ expect(() =>
597
+ TypedData.assert({
598
+ types: {
599
+ Message: [{ name: 'amount', type: 'uint' }],
600
+ },
601
+ primaryType: 'Message',
602
+ message: { amount: 1n },
603
+ } as any),
604
+ ).toThrowErrorMatchingInlineSnapshot(`
605
+ [TypedData.InvalidTypeError: Type "uint" is not a valid EIP-712 type.
606
+
607
+ Use "uint256" instead.]
608
+ `)
609
+ })
610
+
611
+ test('invalid type: undefined struct', () => {
612
+ expect(() =>
613
+ TypedData.assert({
614
+ types: {
615
+ Mail: [{ name: 'from', type: 'Person' }],
616
+ },
617
+ primaryType: 'Mail',
618
+ message: { from: { name: 'Cow' } },
619
+ } as any),
620
+ ).toThrowErrorMatchingInlineSnapshot(
621
+ `[TypedData.InvalidTypeError: Type "Person" is not a valid EIP-712 type.]`,
622
+ )
623
+ })
624
+
625
+ test.each(['bytes0', 'Foo', 'uint', 'uint7', 'uint7[]'])(
626
+ 'invalid type in array element struct: %s',
627
+ (type) => {
628
+ for (const to of ['Person[]', 'Person[][]', 'Person[2]'])
629
+ expect(() =>
630
+ TypedData.assert({
631
+ types: {
632
+ Mail: [{ name: 'to', type: to }],
633
+ Person: [{ name: 'x', type }],
634
+ },
635
+ primaryType: 'Mail',
636
+ message: { to: [] },
637
+ } as any),
638
+ ).toThrowError(`Type "${type}" is not a valid EIP-712 type.`)
639
+ },
640
+ )
641
+
642
+ test.each(['__proto__', 'constructor', 'hasOwnProperty', 'toString'])(
643
+ 'invalid type: %s',
644
+ (type) => {
645
+ expect(() =>
646
+ TypedData.assert({
647
+ types: {
648
+ Message: [{ name: 'value', type }],
649
+ },
650
+ primaryType: 'Message',
651
+ message: { value: {} },
652
+ } as any),
653
+ ).toThrowError(`Type "${type}" is not a valid EIP-712 type.`)
654
+ },
655
+ )
501
656
  })
502
657
 
503
658
  describe('domainSeparator', () => {
@@ -1054,6 +1209,37 @@ describe('getSignPayload', () => {
1054
1209
  `"0xd2669f23b7849020ad41bcbff5b51372793f91320e0f901641945568ed7322be"`,
1055
1210
  )
1056
1211
  })
1212
+ test('struct type: prefixed by a Solidity type', () => {
1213
+ expect(
1214
+ TypedData.getSignPayload({
1215
+ domain: { name: 'Domain' },
1216
+ types: {
1217
+ Message: [{ name: 'intent', type: 'intent' }],
1218
+ intent: [{ name: 'amount', type: 'uint256' }],
1219
+ },
1220
+ primaryType: 'Message',
1221
+ message: { intent: { amount: 1n } },
1222
+ }),
1223
+ ).toMatchInlineSnapshot(
1224
+ `"0xcbeb995e95e87817f47e51736d0e5825644055cbcad196c2b26449ad78fd87ee"`,
1225
+ )
1226
+ })
1227
+
1228
+ test('struct type: with $', () => {
1229
+ expect(
1230
+ TypedData.getSignPayload({
1231
+ domain: { name: 'Domain' },
1232
+ types: {
1233
+ Message: [{ name: 'order', type: 'Order$1' }],
1234
+ Order$1: [{ name: 'amount', type: 'uint256' }],
1235
+ },
1236
+ primaryType: 'Message',
1237
+ message: { order: { amount: 1n } },
1238
+ }),
1239
+ ).toMatchInlineSnapshot(
1240
+ `"0x97a137d1f96a18c6a214fd8e7b991ed7309ff25f223b68aa63a4670f9838a94a"`,
1241
+ )
1242
+ })
1057
1243
  })
1058
1244
 
1059
1245
  describe('hashDomain', () => {
@@ -1613,6 +1799,7 @@ test('exports', () => {
1613
1799
  "InvalidDomainError",
1614
1800
  "InvalidPrimaryTypeError",
1615
1801
  "InvalidStructTypeError",
1802
+ "InvalidTypeError",
1616
1803
  "InvalidArrayError",
1617
1804
  "InvalidArrayLengthError",
1618
1805
  "encodeData",
@@ -4,6 +4,7 @@ import type * as AbiItem from '../AbiItem.js'
4
4
  import type * as AbiParameters from '../AbiParameters.js'
5
5
  import * as Address from '../Address.js'
6
6
  import * as Errors from '../Errors.js'
7
+ import * as Hex from '../Hex.js'
7
8
  import { getArrayComponents } from './abiParameters.js'
8
9
  import type {
9
10
  Compute,
@@ -601,11 +602,32 @@ export function isArgOfType(
601
602
  },
602
603
  )
603
604
 
604
- if (integerTypeRegex.test(abiParameterType))
605
- return argType === 'number' || argType === 'bigint'
605
+ if (integerTypeRegex.test(abiParameterType)) {
606
+ if (typeof arg !== 'bigint' && typeof arg !== 'number') return false
607
+ if (typeof arg === 'number' && !Number.isInteger(arg)) return false
606
608
 
607
- if (fixedBytesTypeRegex.test(abiParameterType))
608
- return argType === 'string' || arg instanceof Uint8Array
609
+ // An overload whose integer range excludes the value does not match.
610
+ const signed = abiParameterType.startsWith('int')
611
+ const bits = Number.parseInt(
612
+ abiParameterType.replace(/^u?int/, '') || '256',
613
+ 10,
614
+ )
615
+ const max = 2n ** (BigInt(bits) - (signed ? 1n : 0n)) - 1n
616
+ const min = signed ? -max - 1n : 0n
617
+ return arg >= min && arg <= max
618
+ }
619
+
620
+ if (fixedBytesTypeRegex.test(abiParameterType)) {
621
+ if (typeof arg !== 'string' && !(arg instanceof Uint8Array))
622
+ return false
623
+ // An overload whose fixed size differs from the value does not match.
624
+ const bytes = abiParameterType.slice(5)
625
+ if (!bytes) return true
626
+ if (arg instanceof Uint8Array)
627
+ return arg.length === Number.parseInt(bytes, 10)
628
+ if (!Hex.validate(arg)) return false
629
+ return Hex.size(arg as Hex.Hex) === Number.parseInt(bytes, 10)
630
+ }
609
631
 
610
632
  // Fixed-length (`<type>[M]`) and dynamic (`<type>[]`) arrays.
611
633
  const arrayComponents = getArrayComponents(abiParameterType)
@@ -683,6 +705,15 @@ export function getAmbiguousTypes(
683
705
  [(args[parameterIndex] as readonly unknown[] | undefined)?.[0]],
684
706
  )
685
707
  if (types) return types
708
+ // Same element type, different array shape: `uint256[]` and `uint256[2]`
709
+ // both accept `[1n, 1n]`, but one is dynamic and the other is static.
710
+ // Skip this when the element types differ. `string[]` vs `address[]`
711
+ // stays unambiguous for a non-address string.
712
+ if (
713
+ sourceArray[1] === targetArray[1] &&
714
+ sourceParameter.type !== targetParameter.type
715
+ )
716
+ return [sourceParameter.type, targetParameter.type]
686
717
  continue
687
718
  }
688
719
 
@@ -690,20 +721,20 @@ export function getAmbiguousTypes(
690
721
  const targetType = targetParameter.type
691
722
  const arg = args[parameterIndex] as Address.Address
692
723
 
693
- let ambiguous = false
694
- if (
695
- (sourceType === 'address' && targetType === 'bytes20') ||
696
- (sourceType === 'bytes20' && targetType === 'address')
697
- )
698
- ambiguous = true
699
- else if (
700
- (sourceType === 'address' && targetType === 'string') ||
701
- (sourceType === 'string' && targetType === 'address') ||
702
- (sourceType === 'address' && targetType === 'bytes') ||
703
- (sourceType === 'bytes' && targetType === 'address')
704
- )
724
+ const ambiguous = (() => {
705
725
  // An `undefined` value comes from an empty array, which matches both types.
706
- ambiguous = arg === undefined || Address.validate(arg, { strict: false })
726
+ // This stays above the catch-all: a non-address string is not ambiguous.
727
+ if (
728
+ (sourceType === 'address' && targetType === 'string') ||
729
+ (sourceType === 'string' && targetType === 'address') ||
730
+ (sourceType === 'address' && targetType === 'bytes') ||
731
+ (sourceType === 'bytes' && targetType === 'address')
732
+ )
733
+ return arg === undefined || Address.validate(arg, { strict: false })
734
+ // Distinct types that both matched cannot be distinguished at runtime
735
+ // (e.g. `bytes` vs `bytes32`, `uint256` vs `int256`, `address` vs `bytes20`).
736
+ return sourceType !== targetType
737
+ })()
707
738
 
708
739
  if (ambiguous) return [sourceType, targetType]
709
740
  }
@@ -178,6 +178,14 @@ export function decodeAddress(
178
178
  ) {
179
179
  const { checksum = false } = options
180
180
  const value = cursor.readBytes(32)
181
+ // An address occupies the low 20 bytes of the word; the high 12 bytes
182
+ // must be zero padding.
183
+ for (let i = 0; i < 12; i++)
184
+ if (value[i] !== 0)
185
+ throw new Address.InvalidAddressError({
186
+ address: Hex.fromBytes(value),
187
+ cause: new Address.InvalidInputError(),
188
+ })
181
189
  const wrap = (address: Hex.Hex) =>
182
190
  checksum ? Address.checksum(address) : address
183
191
  return [wrap(Hex.fromBytes(Bytes.slice(value, -20))), 32]
@@ -185,6 +193,7 @@ export function decodeAddress(
185
193
 
186
194
  export declare namespace decodeAddress {
187
195
  type ErrorType =
196
+ | Address.InvalidAddressError
188
197
  | Hex.fromBytes.ErrorType
189
198
  | Bytes.slice.ErrorType
190
199
  | Errors.GlobalErrorType
@@ -356,19 +365,28 @@ export function decodeNumber(
356
365
  ) {
357
366
  const signed = param.type.startsWith('int')
358
367
  const size = Number.parseInt(param.type.split('int')[1] || '256', 10)
359
- const value = cursor.readBytes(32)
360
- return [
361
- size > 48
362
- ? Bytes.toBigInt(value, { signed })
363
- : Bytes.toNumber(value, { signed }),
364
- 32,
365
- ]
368
+ const value = Bytes.toBigInt(cursor.readBytes(32), { signed })
369
+ // The word must hold a value that fits the declared size (zero-padded for
370
+ // unsigned, sign-extended for signed types).
371
+ if (size < 256) {
372
+ const max = (1n << BigInt(signed ? size - 1 : size)) - 1n
373
+ const min = signed ? -max - 1n : 0n
374
+ if (value > max || value < min)
375
+ throw new Hex.IntegerOutOfRangeError({
376
+ max: max.toString(),
377
+ min: min.toString(),
378
+ signed,
379
+ size: size / 8,
380
+ value: value.toString(),
381
+ })
382
+ }
383
+ return [size > 48 ? value : Number(value), 32]
366
384
  }
367
385
 
368
386
  export declare namespace decodeNumber {
369
387
  type ErrorType =
370
- | Bytes.toNumber.ErrorType
371
388
  | Bytes.toBigInt.ErrorType
389
+ | Hex.IntegerOutOfRangeError
372
390
  | Errors.GlobalErrorType
373
391
  }
374
392
 
@@ -13,6 +13,7 @@ import type * as Transaction from '../../Transaction.js'
13
13
  import type * as TransactionReceipt from '../../TransactionReceipt.js'
14
14
  import type * as TransactionRequest from '../../TransactionRequest.js'
15
15
  import type * as TxEnvelope from '../../TxEnvelope.js'
16
+ import type { OneOf, UnionCompute } from '../types.js'
16
17
 
17
18
  /**
18
19
  * Union of all JSON-RPC Methods for the `eth_` namespace.
@@ -237,7 +238,7 @@ export type Eth = RpcSchema.From<
237
238
  ReturnType: {
238
239
  capabilities?: Record<string, unknown> | undefined
239
240
  raw?: Hex.Hex | undefined
240
- tx: TxEnvelope.Rpc & { from: Address.Address }
241
+ tx: UnionCompute<OneOf<TxEnvelope.Rpc> & { from: Address.Address }>
241
242
  }
242
243
  }
243
244
  /**
@@ -97,8 +97,8 @@ const signature_keychain = {
97
97
 
98
98
  describe('configurable account binding', () => {
99
99
  test('behavior: accepts unbound configurable grants', () => {
100
- // The node binds a root-signed grant to its signer, so only admin-signed
101
- // grants need `account` (TIP-1049).
100
+ // The node binds a grant to its signer, so `account` is optional. From T14
101
+ // only the root key or owner quorum may sign a grant (TIP-1137).
102
102
  const signed = KeyAuthorization.from(
103
103
  { address, chainId: 1n, type: 'configurable' },
104
104
  { signature: signature_secp256k1 },
@@ -2673,7 +2673,9 @@ describe('witness (TIP-1053)', () => {
2673
2673
  })
2674
2674
  })
2675
2675
 
2676
- describe('admin keys (TIP-1049)', () => {
2676
+ // TIP-1137 removes admin keys at T14, but the wire fields are unchanged, so
2677
+ // pre-T14 history (T6 to T13) must still encode, decode, and hash.
2678
+ describe('historical admin keys (TIP-1049)', () => {
2677
2679
  const account = '0x1111111111111111111111111111111111111111' as const
2678
2680
 
2679
2681
  test('from: preserves isAdmin and account', () => {
@@ -2858,7 +2860,7 @@ describe('admin keys (TIP-1049)', () => {
2858
2860
  })
2859
2861
 
2860
2862
  describe('configurable admin keys', () => {
2861
- test('behavior: a configurable account may sign or be granted an admin key', () => {
2863
+ test('behavior: round-trips a pre-T14 configurable admin grant', () => {
2862
2864
  const signed = KeyAuthorization.from(
2863
2865
  {
2864
2866
  account: signature_configurable.account,
@@ -38,7 +38,10 @@ export type KeyAuthorization<
38
38
  bigintType = bigint,
39
39
  numberType = number,
40
40
  > = {
41
- /** Account address this authorization is bound to. Required when an admin access key signs the grant (TIP-1049). */
41
+ /**
42
+ * Account address this authorization is bound to. When present, the Tempo node
43
+ * requires it to equal the transaction sender.
44
+ */
42
45
  account?: Address.Address | undefined
43
46
  /** Address derived from the public key of the key type. */
44
47
  address: Address.Address
@@ -46,7 +49,14 @@ export type KeyAuthorization<
46
49
  chainId: bigintType
47
50
  /** Unix timestamp when key expires (undefined = never expires). */
48
51
  expiry?: numberType | null | undefined
49
- /** Whether this authorization provisions an admin access key. */
52
+ /**
53
+ * Whether this authorization provisions an admin access key (TIP-1049).
54
+ *
55
+ * @deprecated Admin access keys are removed at T14 by
56
+ * [TIP-1137](https://docs.tempo.xyz/protocol/tips/tip-1137). From T14 the Tempo
57
+ * node rejects any key authorization with `isAdmin: true`. The field is still
58
+ * encoded and decoded so pre-T14 history round-trips.
59
+ */
50
60
  isAdmin?: boolean | undefined
51
61
  /**
52
62
  * TIP20 spending limits for this key.
@@ -104,7 +114,7 @@ export type Input = KeyAuthorization<false, bigint, number>
104
114
 
105
115
  /** RPC representation matching the node's wire format. */
106
116
  export type Rpc = {
107
- /** Optional account address binding (TIP-1049). */
117
+ /** Optional account address binding. When present, must equal the transaction sender. */
108
118
  account?: Address.Address | null | undefined
109
119
  /** Allowed call scopes (node field: `allowedCalls`). */
110
120
  allowedCalls?: readonly RpcCallScope[] | null | undefined
@@ -112,7 +122,12 @@ export type Rpc = {
112
122
  chainId: Hex.Hex
113
123
  /** Expiry timestamp (hex quantity or null). */
114
124
  expiry: Hex.Hex | null | undefined
115
- /** Whether this authorization provisions an admin access key (TIP-1049). */
125
+ /**
126
+ * Whether this authorization provisions an admin access key (TIP-1049).
127
+ *
128
+ * @deprecated Rejected from T14 by [TIP-1137](https://docs.tempo.xyz/protocol/tips/tip-1137).
129
+ * Still decoded for pre-T14 history.
130
+ */
116
131
  isAdmin?: boolean | null | undefined
117
132
  /** Key identifier. */
118
133
  keyId: Address.Address
@@ -712,7 +727,9 @@ export function fromTuple<const tuple extends Tuple>(
712
727
  const isAdmin = (() => {
713
728
  if (isAbsent(rawIsAdmin)) return undefined
714
729
  // TIP-1049: the admin marker is strictly `0x01`. Any other value is a
715
- // protocol-level decode error on the node, so reject it here too.
730
+ // protocol-level decode error on the node, so reject it here too. T14
731
+ // (TIP-1137) rejects admin authorizations at validation, not decoding,
732
+ // so pre-T14 history still decodes.
716
733
  if (rawIsAdmin !== '0x01')
717
734
  throw new InvalidAdminMarkerError(rawIsAdmin as Hex.Hex)
718
735
  return true
@@ -1140,8 +1157,9 @@ export function toTuple<const authorization extends KeyAuthorization>(
1140
1157
  { value: witness, placeholder: '0x' },
1141
1158
  // TIP-1049: admin marker. Present = `0x01` (RLP integer 1); absent
1142
1159
  // skipped or omitted. Any other value is a hard decode error on the node.
1160
+ // TIP-1137 keeps this encoding but rejects `0x01` from T14.
1143
1161
  { value: isAdmin ? '0x01' : undefined, placeholder: '0x' },
1144
- // TIP-1049: optional account binding. Last field — never a placeholder.
1162
+ // Optional account binding. Last field — never a placeholder.
1145
1163
  { value: account, placeholder: '0x' },
1146
1164
  ]
1147
1165
  let lastPresent = -1
@@ -1245,8 +1263,8 @@ export class InvalidSignatureTypeError extends Error {
1245
1263
  /**
1246
1264
  * Thrown when a configurable account key grant omits its parent account binding.
1247
1265
  *
1248
- * @deprecated No longer thrown. The Tempo node only requires `account` on grants
1249
- * signed by an admin access key, which it checks against the transaction sender.
1266
+ * @deprecated No longer thrown. The Tempo node does not require `account`; when
1267
+ * present, it must equal the transaction sender.
1250
1268
  */
1251
1269
  export class MissingAccountError extends Error {
1252
1270
  override readonly name = 'KeyAuthorization.MissingAccountError'
@@ -106,6 +106,19 @@ describe('assert', () => {
106
106
  )
107
107
  })
108
108
 
109
+ test('tip above zero fee cap', () => {
110
+ expect(() =>
111
+ TxEnvelopeTempo.assert({
112
+ calls: [{ to: '0x0000000000000000000000000000000000000000' }],
113
+ chainId: 1,
114
+ maxFeePerGas: 0n,
115
+ maxPriorityFeePerGas: 1n,
116
+ }),
117
+ ).toThrowErrorMatchingInlineSnapshot(
118
+ `[TransactionEnvelope.TipAboveFeeCapError: The provided tip (\`maxPriorityFeePerGas\` = 0.000000001 gwei) cannot be higher than the fee cap (\`maxFeePerGas\` = 0 gwei).]`,
119
+ )
120
+ })
121
+
109
122
  test('invalid chain id', () => {
110
123
  expect(() =>
111
124
  TxEnvelopeTempo.assert({
@@ -138,6 +151,19 @@ describe('deserialize', () => {
138
151
  expect(deserialized).toEqual(transaction)
139
152
  })
140
153
 
154
+ test('tip above empty fee cap', () => {
155
+ const serialized = TxEnvelopeTempo.serialize({
156
+ calls: [{ to: '0x0000000000000000000000000000000000000000' }],
157
+ chainId: 1,
158
+ maxPriorityFeePerGas: 1n,
159
+ })
160
+ expect(() =>
161
+ TxEnvelopeTempo.deserialize(serialized),
162
+ ).toThrowErrorMatchingInlineSnapshot(
163
+ `[TransactionEnvelope.TipAboveFeeCapError: The provided tip (\`maxPriorityFeePerGas\` = 0.000000001 gwei) cannot be higher than the fee cap (\`maxFeePerGas\` = 0 gwei).]`,
164
+ )
165
+ })
166
+
141
167
  test('fee payer', () => {
142
168
  const serialized = TxEnvelopeTempo.serialize(transaction, {
143
169
  format: 'feePayer',
@@ -214,8 +214,8 @@ export function assert(envelope: PartialBy<TxEnvelopeTempo, 'type'>) {
214
214
  })
215
215
 
216
216
  if (
217
- maxPriorityFeePerGas &&
218
- maxFeePerGas &&
217
+ typeof maxPriorityFeePerGas === 'bigint' &&
218
+ typeof maxFeePerGas === 'bigint' &&
219
219
  maxPriorityFeePerGas > maxFeePerGas
220
220
  )
221
221
  throw new TransactionEnvelope.TipAboveFeeCapError({
@@ -335,6 +335,12 @@ export function deserialize(serialized: Serialized): Compute<TxEnvelopeTempo> {
335
335
  transaction.maxFeePerGas = BigInt(maxFeePerGas)
336
336
  if (Hex.validate(maxPriorityFeePerGas) && maxPriorityFeePerGas !== '0x')
337
337
  transaction.maxPriorityFeePerGas = BigInt(maxPriorityFeePerGas)
338
+ // An empty `maxFeePerGas` encodes zero, so a tip above it is rejected.
339
+ if (
340
+ transaction.maxPriorityFeePerGas &&
341
+ typeof transaction.maxFeePerGas === 'undefined'
342
+ )
343
+ transaction.maxFeePerGas = 0n
338
344
  if (Hex.validate(nonceKey))
339
345
  transaction.nonceKey = nonceKey === '0x' ? 0n : BigInt(nonceKey)
340
346
  if (Hex.validate(validBefore) && validBefore !== '0x')