ox 1.8.3 → 1.8.4

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 (69) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/core/Transaction.d.ts +5 -2
  3. package/dist/core/Transaction.d.ts.map +1 -1
  4. package/dist/core/Transaction.js +8 -0
  5. package/dist/core/Transaction.js.map +1 -1
  6. package/dist/core/TransactionRequest.d.ts +5 -2
  7. package/dist/core/TransactionRequest.d.ts.map +1 -1
  8. package/dist/core/TransactionRequest.js +8 -0
  9. package/dist/core/TransactionRequest.js.map +1 -1
  10. package/dist/core/TxEnvelope.d.ts +1 -1
  11. package/dist/core/TxEnvelope.d.ts.map +1 -1
  12. package/dist/core/TxEnvelope.js +2 -1
  13. package/dist/core/TxEnvelope.js.map +1 -1
  14. package/dist/core/TxEnvelopeEip8141.d.ts +18 -7
  15. package/dist/core/TxEnvelopeEip8141.d.ts.map +1 -1
  16. package/dist/core/TxEnvelopeEip8141.js +37 -8
  17. package/dist/core/TxEnvelopeEip8141.js.map +1 -1
  18. package/dist/core/internal/frameNonce.d.ts +10 -0
  19. package/dist/core/internal/frameNonce.d.ts.map +1 -0
  20. package/dist/core/internal/frameNonce.js +45 -0
  21. package/dist/core/internal/frameNonce.js.map +1 -0
  22. package/dist/zod/Block.d.ts +3 -0
  23. package/dist/zod/Block.d.ts.map +1 -1
  24. package/dist/zod/RpcSchema.d.ts +23 -0
  25. package/dist/zod/RpcSchema.d.ts.map +1 -1
  26. package/dist/zod/Transaction.d.ts +6 -0
  27. package/dist/zod/Transaction.d.ts.map +1 -1
  28. package/dist/zod/Transaction.js +18 -11
  29. package/dist/zod/Transaction.js.map +1 -1
  30. package/dist/zod/TransactionEnvelope.d.ts +10 -0
  31. package/dist/zod/TransactionEnvelope.d.ts.map +1 -1
  32. package/dist/zod/TransactionRequest.d.ts +2 -0
  33. package/dist/zod/TransactionRequest.d.ts.map +1 -1
  34. package/dist/zod/TransactionRequest.js +9 -3
  35. package/dist/zod/TransactionRequest.js.map +1 -1
  36. package/dist/zod/TxEnvelopeEip8141.d.ts +7 -0
  37. package/dist/zod/TxEnvelopeEip8141.d.ts.map +1 -1
  38. package/dist/zod/TxEnvelopeEip8141.js +2 -0
  39. package/dist/zod/TxEnvelopeEip8141.js.map +1 -1
  40. package/dist/zod/internal/Frame.d.ts +8 -0
  41. package/dist/zod/internal/Frame.d.ts.map +1 -1
  42. package/dist/zod/internal/Frame.js +29 -0
  43. package/dist/zod/internal/Frame.js.map +1 -1
  44. package/dist/zod/internal/rpcSchemas/Eth.d.ts +23 -0
  45. package/dist/zod/internal/rpcSchemas/Eth.d.ts.map +1 -1
  46. package/dist/zod/internal/rpcSchemas/Wallet.d.ts +2 -0
  47. package/dist/zod/internal/rpcSchemas/Wallet.d.ts.map +1 -1
  48. package/dist/zod/tempo/RpcSchemaTempo.d.ts +4 -0
  49. package/dist/zod/tempo/RpcSchemaTempo.d.ts.map +1 -1
  50. package/dist/zod/tempo/Transaction.d.ts +3 -0
  51. package/dist/zod/tempo/Transaction.d.ts.map +1 -1
  52. package/package.json +1 -1
  53. package/src/core/Transaction.ts +17 -0
  54. package/src/core/TransactionRequest.ts +12 -0
  55. package/src/core/TxEnvelope.ts +7 -2
  56. package/src/core/TxEnvelopeEip8141.ts +61 -13
  57. package/src/core/_test/TransactionEnvelope.test-d.ts +27 -0
  58. package/src/core/_test/TransactionEnvelope.test.ts +26 -0
  59. package/src/core/_test/TransactionRequest.test.ts +28 -0
  60. package/src/core/_test/TxEnvelopeEip8141.test-d.ts +27 -0
  61. package/src/core/_test/TxEnvelopeEip8141.test.ts +456 -2
  62. package/src/core/internal/frameNonce.ts +61 -0
  63. package/src/version.ts +1 -1
  64. package/src/zod/Transaction.ts +42 -23
  65. package/src/zod/TransactionRequest.ts +32 -17
  66. package/src/zod/TxEnvelopeEip8141.ts +2 -0
  67. package/src/zod/_test/TxEnvelopeEip8141.nonce.test.ts +174 -0
  68. package/src/zod/_test/TxEnvelopeEip8141.test-d.ts +29 -0
  69. package/src/zod/internal/Frame.ts +45 -0
@@ -1,3 +1,4 @@
1
+ import { encodeRlp, keccak256 } from 'ethers'
1
2
  import {
2
3
  Address,
3
4
  Blobs,
@@ -402,7 +403,7 @@ describe('deserialize', () => {
402
403
  expect(() =>
403
404
  TxEnvelopeEip8141.deserialize('0x06c0' as TxEnvelopeEip8141.Serialized),
404
405
  ).toThrowErrorMatchingInlineSnapshot(
405
- `[TxEnvelopeEip8141.InvalidError: Expected seven transaction fields.]`,
406
+ `[TxEnvelopeEip8141.InvalidError: Expected seven or eight transaction fields.]`,
406
407
  )
407
408
  })
408
409
 
@@ -426,7 +427,7 @@ describe('deserialize', () => {
426
427
  expect(() =>
427
428
  TxEnvelopeEip8141.deserialize('0x06c180' as TxEnvelopeEip8141.Serialized),
428
429
  ).toThrowErrorMatchingInlineSnapshot(
429
- `[TxEnvelopeEip8141.InvalidError: Expected seven transaction fields.]`,
430
+ `[TxEnvelopeEip8141.InvalidError: Expected seven or eight transaction fields.]`,
430
431
  )
431
432
  })
432
433
 
@@ -1812,3 +1813,456 @@ describe('fromRpc', () => {
1812
1813
  )
1813
1814
  })
1814
1815
  })
1816
+
1817
+ describe('keyed nonces', () => {
1818
+ const sender = '0x1111111111111111111111111111111111111111'
1819
+ const envelope = TxEnvelopeEip8141.from({
1820
+ chainId: 1,
1821
+ frames: [{ mode: 'sender' }],
1822
+ sender,
1823
+ signatures: [{ scheme: 'arbitrary', signature: '0xdeadbeef' }],
1824
+ })
1825
+
1826
+ // Generated independently with ethers 6.17 from the literal EIP-8250 RLP layout.
1827
+ const vectors = [
1828
+ {
1829
+ nonceKeys: undefined,
1830
+ nonce: 0n,
1831
+ serialized:
1832
+ '0x06f00180941111111111111111111111111111111111111111c9c8028080c280808080c9c880808084deadbeefc3808080c0',
1833
+ hash: '0xa14c90dee4c545369e201e4975ee2ef415357df20a0a183d1be9f602bac9a47d',
1834
+ signHash:
1835
+ '0x5682abf965767351637964eed1fe3f775ab45de426def7da68736cf2ae0a43b5',
1836
+ },
1837
+ {
1838
+ nonceKeys: [0n],
1839
+ nonce: 0n,
1840
+ serialized:
1841
+ '0x06f201c18080941111111111111111111111111111111111111111c9c8028080c280808080c9c880808084deadbeefc3808080c0',
1842
+ hash: '0x069a1528377db98ed06429aab0dad8043315859ac454ce3e1f5a250e365d072d',
1843
+ signHash:
1844
+ '0xd573654ae8d57432b3856b0078fb98f2f3e4d3f73a023d7ba8a952a16d0f4726',
1845
+ },
1846
+ {
1847
+ nonceKeys: [1n, 128n, 2n ** 256n - 1n],
1848
+ nonce: 2n ** 64n - 2n,
1849
+ serialized:
1850
+ '0x06f85d01e4018180a0ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff88fffffffffffffffe941111111111111111111111111111111111111111c9c8028080c280808080c9c880808084deadbeefc3808080c0',
1851
+ hash: '0x620915f063bdd017a09ae856d0043d064568b3455aba741d7ebc748731fc503f',
1852
+ signHash:
1853
+ '0x3d28f5f76f353611385be1512c2832220686b27cad3c53d5b07941de1515319b',
1854
+ },
1855
+ ] as const
1856
+
1857
+ describe('serialize', () => {
1858
+ test.each(vectors)(
1859
+ 'independent encoding and signing vector %#',
1860
+ (vector) => {
1861
+ const value = {
1862
+ ...envelope,
1863
+ nonce: vector.nonce,
1864
+ nonceKeys: vector.nonceKeys,
1865
+ }
1866
+ expect(TxEnvelopeEip8141.serialize(value)).toBe(vector.serialized)
1867
+ expect(TxEnvelopeEip8141.hash(value)).toBe(vector.hash)
1868
+ expect(TxEnvelopeEip8141.getSignPayload(value)).toBe(vector.signHash)
1869
+ expect(TransactionEnvelope.serialize(value)).toBe(vector.serialized)
1870
+ expect(TransactionEnvelope.getSignPayload(value)).toBe(vector.signHash)
1871
+ const decoded = TransactionEnvelope.deserialize(vector.serialized)
1872
+ expect(TxEnvelopeEip8141.serialize(decoded)).toBe(vector.serialized)
1873
+ expect(decoded.nonceKeys).toEqual(vector.nonceKeys)
1874
+ expect(decoded.nonce).toBe(vector.nonce)
1875
+ expect(TxEnvelopeEip8141.getSignPayload(decoded)).toBe(vector.signHash)
1876
+ },
1877
+ )
1878
+
1879
+ test('matches independent literal RLP layout', () => {
1880
+ const body = [
1881
+ '0x01',
1882
+ ['0x01', '0x8180'],
1883
+ '0x05',
1884
+ sender,
1885
+ [['0x02', '0x', '0x', ['0x', '0x'], '0x', '0x']],
1886
+ [['0x', '0x', '0x', '0xdeadbeef']],
1887
+ ['0x', '0x', '0x'],
1888
+ [],
1889
+ ]
1890
+ const serialized = `0x06${encodeRlp(body).slice(2)}`
1891
+ const value = { ...envelope, nonceKeys: [1n, 33152n], nonce: 5n }
1892
+ expect(TxEnvelopeEip8141.serialize(value)).toBe(serialized)
1893
+ expect(TxEnvelopeEip8141.hash(value)).toBe(keccak256(serialized))
1894
+ })
1895
+
1896
+ test('keyed PeerDAS wrapper preserves keys and excludes sidecars from hashes', () => {
1897
+ const commitment = `0x${'00'.repeat(48)}` as const
1898
+ const value = {
1899
+ ...envelope,
1900
+ nonceKeys: [1n],
1901
+ blobVersionedHashes: [Blobs.commitmentToVersionedHash(commitment)],
1902
+ sidecars: {
1903
+ blobs: [`0x${'00'.repeat(Blobs.bytesPerBlob)}` as const],
1904
+ commitments: [commitment],
1905
+ cellProofs: Array.from({ length: 128 }, () => commitment),
1906
+ },
1907
+ }
1908
+ const serialized = TxEnvelopeEip8141.serialize(value)
1909
+ const decoded = TxEnvelopeEip8141.deserialize(serialized)
1910
+ expect(decoded.nonceKeys).toMatchInlineSnapshot(`
1911
+ [
1912
+ 1n,
1913
+ ]
1914
+ `)
1915
+ expect(TxEnvelopeEip8141.serialize(decoded)).toBe(serialized)
1916
+ expect(TxEnvelopeEip8141.hash(value)).toBe(
1917
+ TxEnvelopeEip8141.hash({ ...value, sidecars: undefined }),
1918
+ )
1919
+ expect(TxEnvelopeEip8141.getSignPayload(value)).toBe(
1920
+ TxEnvelopeEip8141.getSignPayload({ ...value, sidecars: undefined }),
1921
+ )
1922
+ })
1923
+ })
1924
+
1925
+ describe('assert', () => {
1926
+ test.each([
1927
+ [],
1928
+ [0n, 1n],
1929
+ [1n, 1n],
1930
+ [2n, 1n],
1931
+ [-1n],
1932
+ [2n ** 256n],
1933
+ Array.from({ length: 17 }, (_, i) => BigInt(i + 1)),
1934
+ ])('rejects invalid keys %#', (...nonceKeys) => {
1935
+ expect(TxEnvelopeEip8141.validate({ ...envelope, nonceKeys })).toBe(false)
1936
+ expect(() =>
1937
+ TxEnvelopeEip8141.serialize({ ...envelope, nonceKeys }),
1938
+ ).toThrow()
1939
+ })
1940
+
1941
+ test('accepts 16 keys and the maximum key', () => {
1942
+ expect(
1943
+ TxEnvelopeEip8141.validate({
1944
+ ...envelope,
1945
+ nonceKeys: Array.from({ length: 16 }, (_, i) => BigInt(i + 1)),
1946
+ }),
1947
+ ).toBe(true)
1948
+ expect(
1949
+ TxEnvelopeEip8141.validate({
1950
+ ...envelope,
1951
+ nonceKeys: [2n ** 256n - 1n],
1952
+ }),
1953
+ ).toBe(true)
1954
+ })
1955
+
1956
+ test.each([-1n, 2n ** 64n - 1n, 2n ** 64n])(
1957
+ 'rejects invalid shared sequence %s',
1958
+ (nonce) => {
1959
+ expect(() =>
1960
+ TxEnvelopeEip8141.assert({ ...envelope, nonceKeys: [1n], nonce }),
1961
+ ).toThrowErrorMatchingInlineSnapshot(
1962
+ `[FrameNonce.InvalidError: Nonce must be less than 2^64 - 1 and nonnegative.]`,
1963
+ )
1964
+ },
1965
+ )
1966
+
1967
+ test('rejects a numeric key or null list', () => {
1968
+ // @ts-expect-error invalid runtime input
1969
+ expect(TxEnvelopeEip8141.validate({ ...envelope, nonceKeys: [1] })).toBe(
1970
+ false,
1971
+ )
1972
+ // @ts-expect-error invalid runtime input
1973
+ expect(TxEnvelopeEip8141.validate({ ...envelope, nonceKeys: null })).toBe(
1974
+ false,
1975
+ )
1976
+ })
1977
+
1978
+ test('charges encoded nonce calldata against the transaction gas cap', () => {
1979
+ const value = {
1980
+ ...envelope,
1981
+ signatures: [],
1982
+ frames: [{ executionGas: 16_764_741n }],
1983
+ }
1984
+ expect(TxEnvelopeEip8141.validate(value)).toBe(true)
1985
+ expect(TxEnvelopeEip8141.validate({ ...value, nonceKeys: [0n] })).toBe(
1986
+ false,
1987
+ )
1988
+ // rlp([0]) || rlp(0) = c1 80 80, three nonzero bytes at 16 gas each.
1989
+ expect(
1990
+ TxEnvelopeEip8141.validate({
1991
+ ...value,
1992
+ nonceKeys: [0n],
1993
+ frames: [{ executionGas: 16_764_693n }],
1994
+ }),
1995
+ ).toBe(true)
1996
+ })
1997
+ })
1998
+
1999
+ describe('deserialize', () => {
2000
+ test.each([
2001
+ ['0x', '0x'],
2002
+ [[], '0x'],
2003
+ [['0x00'], '0x'],
2004
+ [['0x0001'], '0x'],
2005
+ [[['0x01']], '0x'],
2006
+ [['0x01'], '0x00'],
2007
+ [['0x01'], '0x0001'],
2008
+ [['0x01'], []],
2009
+ [['0x01', '0x01'], '0x'],
2010
+ [['0x02', '0x01'], '0x'],
2011
+ [['0x', '0x01'], '0x'],
2012
+ [[`0x01${'00'.repeat(32)}`], '0x'],
2013
+ [['0x01'], '0x010000000000000000'],
2014
+ ])('rejects malformed nonce fields %#', (keys, nonce) => {
2015
+ const serialized =
2016
+ `0x06${encodeRlp(['0x01', keys, nonce, sender, [['0x02', '0x', '0x', ['0x', '0x'], '0x', '0x']], [], ['0x', '0x', '0x'], []]).slice(2)}` as const
2017
+ expect(() => TxEnvelopeEip8141.deserialize(serialized)).toThrow()
2018
+ })
2019
+
2020
+ test('rejects nonminimal RLP string and list lengths', () => {
2021
+ const canonical = vectors[1].serialized
2022
+ expect(() =>
2023
+ TxEnvelopeEip8141.deserialize(
2024
+ canonical.replace(
2025
+ 'f201c180',
2026
+ 'f301c28100',
2027
+ ) as TxEnvelopeEip8141.Serialized,
2028
+ ),
2029
+ ).toThrow()
2030
+ expect(() =>
2031
+ TxEnvelopeEip8141.deserialize(
2032
+ canonical.replace(
2033
+ 'f201c180',
2034
+ 'f301f80180',
2035
+ ) as TxEnvelopeEip8141.Serialized,
2036
+ ),
2037
+ ).toThrow()
2038
+ expect(() =>
2039
+ TxEnvelopeEip8141.deserialize(
2040
+ canonical.replace('f201', 'f83201') as TxEnvelopeEip8141.Serialized,
2041
+ ),
2042
+ ).toThrow()
2043
+ })
2044
+ })
2045
+
2046
+ describe('getSignPayload', () => {
2047
+ test('signatures bind the complete key set and sequence', () => {
2048
+ const value = {
2049
+ ...envelope,
2050
+ nonceKeys: [1n, 2n],
2051
+ nonce: 0n,
2052
+ signatures: [{ scheme: 'secp256k1' } as const],
2053
+ }
2054
+ const privateKey =
2055
+ '0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'
2056
+ const publicKey = Secp256k1.getPublicKey({ privateKey })
2057
+ const signature = Secp256k1.sign({
2058
+ privateKey,
2059
+ payload: TxEnvelopeEip8141.getSignPayload(value),
2060
+ })
2061
+ const signed = {
2062
+ ...value,
2063
+ signatures: [{ scheme: 'secp256k1', signature } as const],
2064
+ }
2065
+ expect(
2066
+ Secp256k1.verify({
2067
+ publicKey,
2068
+ signature,
2069
+ payload: TxEnvelopeEip8141.getSignPayload(signed),
2070
+ }),
2071
+ ).toBe(true)
2072
+ expect(
2073
+ Secp256k1.verify({
2074
+ publicKey,
2075
+ signature,
2076
+ payload: TxEnvelopeEip8141.getSignPayload({
2077
+ ...signed,
2078
+ nonceKeys: [1n, 3n],
2079
+ }),
2080
+ }),
2081
+ ).toBe(false)
2082
+ expect(
2083
+ Secp256k1.verify({
2084
+ publicKey,
2085
+ signature,
2086
+ payload: TxEnvelopeEip8141.getSignPayload({ ...signed, nonce: 1n }),
2087
+ }),
2088
+ ).toBe(false)
2089
+ })
2090
+
2091
+ test('commits to keys and shared sequence without mutating signatures', () => {
2092
+ const value = { ...envelope, nonceKeys: [1n, 2n], nonce: 3n }
2093
+ const hash = TxEnvelopeEip8141.getSignPayload(value)
2094
+ expect(
2095
+ TxEnvelopeEip8141.getSignPayload({ ...value, nonceKeys: [1n, 3n] }),
2096
+ ).not.toBe(hash)
2097
+ expect(
2098
+ TxEnvelopeEip8141.getSignPayload({ ...value, nonce: 4n }),
2099
+ ).not.toBe(hash)
2100
+ expect(
2101
+ TxEnvelopeEip8141.getSignPayload({ ...value, nonceKeys: [1n] }),
2102
+ ).not.toBe(hash)
2103
+ expect(
2104
+ TxEnvelopeEip8141.getSignPayload({
2105
+ ...value,
2106
+ signatures: [{ scheme: 'arbitrary', signature: '0xabcdef' }],
2107
+ }),
2108
+ ).toBe(hash)
2109
+ expect(value.signatures[0].signature).toBe('0xdeadbeef')
2110
+ })
2111
+
2112
+ test('retains explicit-message signatures', () => {
2113
+ const value = {
2114
+ ...envelope,
2115
+ nonceKeys: [1n],
2116
+ signatures: [
2117
+ {
2118
+ scheme: 'arbitrary',
2119
+ payload: `0x${'11'.repeat(32)}`,
2120
+ signature: '0xdeadbeef',
2121
+ } as const,
2122
+ ],
2123
+ }
2124
+ expect(TxEnvelopeEip8141.getSignPayload(value)).toBe(
2125
+ TxEnvelopeEip8141.hash(value),
2126
+ )
2127
+ expect(
2128
+ TxEnvelopeEip8141.getSignPayload({
2129
+ ...value,
2130
+ signatures: [{ ...value.signatures[0]!, signature: '0xabcdef' }],
2131
+ }),
2132
+ ).not.toBe(TxEnvelopeEip8141.getSignPayload(value))
2133
+ })
2134
+ })
2135
+
2136
+ describe('toRpc', () => {
2137
+ test.each(vectors)(
2138
+ 'preserves absent or explicit nonce keys %#',
2139
+ (vector) => {
2140
+ const value = {
2141
+ ...envelope,
2142
+ nonceKeys: vector.nonceKeys,
2143
+ nonce: vector.nonce,
2144
+ }
2145
+ const rpc = TxEnvelopeEip8141.toRpc(value)
2146
+ expect('nonceSeq' in rpc).toBe(false)
2147
+ expect('nonceKeys' in rpc).toBe(vector.nonceKeys !== undefined)
2148
+ expect(rpc.nonceKeys).toEqual(
2149
+ vector.nonceKeys?.map((key) => Hex.fromNumber(key)),
2150
+ )
2151
+ expect(rpc.nonce).toBe(Hex.fromNumber(vector.nonce))
2152
+ expect(
2153
+ TxEnvelopeEip8141.serialize(TxEnvelopeEip8141.fromRpc(rpc)),
2154
+ ).toBe(vector.serialized)
2155
+ const request = TransactionEnvelope.toTransactionRequest(value)
2156
+ expect(request.nonceKeys).toEqual(vector.nonceKeys)
2157
+ expect(
2158
+ TransactionEnvelope.serialize(TransactionRequest.toEnvelope(request)),
2159
+ ).toBe(vector.serialized)
2160
+ },
2161
+ )
2162
+
2163
+ test('request conversion preserves omitted sequence and frame gas', () => {
2164
+ const request = {
2165
+ type: 'eip8141',
2166
+ nonceKeys: [1n, 2n],
2167
+ frames: [{ mode: 'sender' }, { mode: 'sender', executionGas: 0n }],
2168
+ } as const
2169
+ const rpc = TransactionRequest.toRpc(request)
2170
+ expect(rpc.nonce).toBeUndefined()
2171
+ expect(rpc.nonceKeys).toMatchInlineSnapshot(`
2172
+ [
2173
+ "0x1",
2174
+ "0x2",
2175
+ ]
2176
+ `)
2177
+ expect(rpc.frames?.map((frame) => frame.executionGas)).toEqual([
2178
+ undefined,
2179
+ '0x0',
2180
+ ])
2181
+ expect(TransactionRequest.fromRpc(rpc).nonceKeys).toEqual(
2182
+ request.nonceKeys,
2183
+ )
2184
+ expect(TransactionRequest.toRpc({ ...request, nonce: 0n }).nonce).toBe(
2185
+ '0x0',
2186
+ )
2187
+ })
2188
+
2189
+ test('transaction result conversion retains keyed nonces', () => {
2190
+ const rpc = {
2191
+ ...TxEnvelopeEip8141.toRpc({
2192
+ ...envelope,
2193
+ nonceKeys: [1n, 2n],
2194
+ nonce: 3n,
2195
+ }),
2196
+ blockHash: null,
2197
+ blockNumber: null,
2198
+ transactionIndex: null,
2199
+ hash: `0x${'11'.repeat(32)}` as const,
2200
+ }
2201
+ const value = Transaction.fromRpc(rpc, { pending: true })!
2202
+ expect(value.nonceKeys).toMatchInlineSnapshot(`
2203
+ [
2204
+ 1n,
2205
+ 2n,
2206
+ ]
2207
+ `)
2208
+ expect(Transaction.toRpc(value, { pending: true }).nonceKeys).toEqual(
2209
+ rpc.nonceKeys,
2210
+ )
2211
+ expect(value.nonce).toBe(3n)
2212
+ })
2213
+
2214
+ test('accepts numberish keys without losing uint256 precision', () => {
2215
+ expect(
2216
+ TxEnvelopeEip8141.toRpc({
2217
+ ...envelope,
2218
+ nonceKeys: ['0x1', 128, 2n ** 256n - 1n],
2219
+ nonce: '0x0',
2220
+ }).nonceKeys,
2221
+ ).toEqual(['0x1', '0x80', `0x${'ff'.repeat(32)}`])
2222
+ })
2223
+ })
2224
+
2225
+ describe('fromRpc', () => {
2226
+ test('requires the sequence in keyed transaction results', () => {
2227
+ const value = {
2228
+ ...TxEnvelopeEip8141.toRpc({ ...envelope, nonceKeys: [1n] }),
2229
+ nonce: undefined,
2230
+ }
2231
+ expect(() =>
2232
+ Transaction.fromRpc(value as never),
2233
+ ).toThrowErrorMatchingInlineSnapshot(
2234
+ `[FrameNonce.InvalidError: A keyed transaction result requires nonce.]`,
2235
+ )
2236
+ })
2237
+
2238
+ test.each([
2239
+ '0x00',
2240
+ '0x01',
2241
+ '0x',
2242
+ '0x10000000000000000',
2243
+ '0xffffffffffffffff',
2244
+ ])('rejects invalid keyed sequence %s', (nonce) => {
2245
+ expect(() =>
2246
+ TxEnvelopeEip8141.fromRpc({
2247
+ ...TxEnvelopeEip8141.toRpc(envelope),
2248
+ nonceKeys: ['0x1'],
2249
+ nonce: nonce as Hex.Hex,
2250
+ }),
2251
+ ).toThrow()
2252
+ expect(() =>
2253
+ TransactionRequest.fromRpc({
2254
+ nonceKeys: ['0x1'],
2255
+ nonce: nonce as Hex.Hex,
2256
+ }),
2257
+ ).toThrow()
2258
+ })
2259
+ test.each([['0x01'], ['0x'], ['0x0', '0x1'], ['0x2', '0x1']])(
2260
+ 'rejects invalid RPC keys %j',
2261
+ (...nonceKeys) => {
2262
+ expect(() =>
2263
+ TransactionRequest.fromRpc({ nonceKeys: nonceKeys as Hex.Hex[] }),
2264
+ ).toThrow()
2265
+ },
2266
+ )
2267
+ })
2268
+ })
@@ -0,0 +1,61 @@
1
+ import * as Errors from '../Errors.js'
2
+ import * as Hex from '../Hex.js'
3
+ import * as Quantity from './quantity.js'
4
+
5
+ export function assert(keys: readonly bigint[], nonce?: bigint): void {
6
+ if (!Array.isArray(keys) || keys.length < 1 || keys.length > 16)
7
+ throw new InvalidError('Expected between 1 and 16 nonce keys.')
8
+ for (const [index, key] of keys.entries()) {
9
+ if (typeof key !== 'bigint' || key < 0n || key >= 2n ** 256n)
10
+ throw new InvalidError('Nonce keys must be unsigned 256-bit integers.')
11
+ if (index > 0 && key <= keys[index - 1]!)
12
+ throw new InvalidError('Nonce keys must be strictly increasing.')
13
+ }
14
+ if (keys.length > 1 && keys[0] === 0n)
15
+ throw new InvalidError(
16
+ 'The zero nonce key cannot be combined with other keys.',
17
+ )
18
+ if (
19
+ nonce !== undefined &&
20
+ (typeof nonce !== 'bigint' || nonce < 0n || nonce >= 2n ** 64n - 1n)
21
+ )
22
+ throw new InvalidError('Nonce must be less than 2^64 - 1 and nonnegative.')
23
+ }
24
+
25
+ export function fromRpc(
26
+ keys: readonly Hex.Hex[],
27
+ nonce?: Hex.Hex,
28
+ ): readonly bigint[] {
29
+ if (!Array.isArray(keys)) throw new InvalidError('Expected a nonce key list.')
30
+ const integer = (value: Hex.Hex): bigint => {
31
+ if (
32
+ typeof value !== 'string' ||
33
+ !/^0x(?:0|[1-9a-fA-F][0-9a-fA-F]*)$/.test(value)
34
+ )
35
+ throw new InvalidError('Expected a canonical RPC nonce quantity.')
36
+ return Hex.toBigInt(value)
37
+ }
38
+ const decoded = keys.map(integer)
39
+ assert(decoded, nonce === undefined ? undefined : integer(nonce))
40
+ return decoded
41
+ }
42
+
43
+ export function toRpc(
44
+ keys: readonly (Hex.Hex | bigint | number)[],
45
+ nonce?: Hex.Hex | bigint | number,
46
+ ): readonly Hex.Hex[] {
47
+ if (!Array.isArray(keys)) throw new InvalidError('Expected a nonce key list.')
48
+ const encoded = keys.map((key) => Quantity.fromNumberish(key))
49
+ fromRpc(
50
+ encoded,
51
+ nonce === undefined ? undefined : Quantity.fromNumberish(nonce),
52
+ )
53
+ return encoded
54
+ }
55
+
56
+ export class InvalidError extends Errors.BaseError {
57
+ override readonly name = 'FrameNonce.InvalidError'
58
+ constructor(reason: string) {
59
+ super(reason)
60
+ }
61
+ }
package/src/version.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  /** @internal */
2
- export const version = '1.8.3'
2
+ export const version = '1.8.4'
@@ -4,7 +4,13 @@ import * as z_Address from './Address.js'
4
4
  import * as z_Authorization from './Authorization.js'
5
5
  import * as z_Frame from './Frame.js'
6
6
  import * as z_FrameSignature from './FrameSignature.js'
7
- import { chainId, chainIdToRpc } from './internal/Frame.js'
7
+ import {
8
+ chainId,
9
+ chainIdToRpc,
10
+ nonceKeys,
11
+ nonceKeysToRpc,
12
+ validNonce,
13
+ } from './internal/Frame.js'
8
14
  import * as z_Hex from './Hex.js'
9
15
  import * as z_Number from './Number.js'
10
16
  import * as z_Uint from './Uint.js'
@@ -142,37 +148,49 @@ export const PendingEip7702 = z.object(
142
148
  const Eip8141Type = type('0x6', 'eip8141')
143
149
 
144
150
  /** EIP-8141 transaction schema. */
145
- export const Eip8141 = z.object({
146
- ...eip8141Fields(z_Uint.Uint, chainId, z_Frame.Frame),
147
- blockHash: z_Hex.Hex,
148
- blockNumber: z_Uint.Uint,
149
- blockTimestamp: z.optional(z_Uint.Uint),
150
- transactionIndex: z_Number.Number,
151
- })
151
+ export const Eip8141 = z
152
+ .object({
153
+ ...eip8141Fields(z_Uint.Uint, chainId, z_Frame.Frame, nonceKeys),
154
+ blockHash: z_Hex.Hex,
155
+ blockNumber: z_Uint.Uint,
156
+ blockTimestamp: z.optional(z_Uint.Uint),
157
+ transactionIndex: z_Number.Number,
158
+ })
159
+ .check(z.refine(validNonce, 'Invalid keyed nonce'))
152
160
 
153
161
  /** Encode-only EIP-8141 transaction schema accepting numberish values. */
154
- export const Eip8141ToRpc = z.object({
155
- ...eip8141Fields(z_Uint.UintToRpc, chainIdToRpc, z_Frame.FrameToRpc),
156
- blockHash: z_Hex.Hex,
157
- blockNumber: z_Uint.UintToRpc,
158
- blockTimestamp: z.optional(z_Uint.UintToRpc),
159
- transactionIndex: z_Number.NumberToRpc,
160
- })
162
+ export const Eip8141ToRpc = z
163
+ .object({
164
+ ...eip8141Fields(
165
+ z_Uint.UintToRpc,
166
+ chainIdToRpc,
167
+ z_Frame.FrameToRpc,
168
+ nonceKeysToRpc,
169
+ ),
170
+ blockHash: z_Hex.Hex,
171
+ blockNumber: z_Uint.UintToRpc,
172
+ blockTimestamp: z.optional(z_Uint.UintToRpc),
173
+ transactionIndex: z_Number.NumberToRpc,
174
+ })
175
+ .check(z.refine(validNonce, 'Invalid keyed nonce'))
161
176
 
162
177
  /** Pending EIP-8141 transaction schema. */
163
- export const PendingEip8141 = z.object({
164
- ...eip8141Fields(z_Uint.Uint, chainId, z_Frame.Frame),
165
- blockHash: z.null(),
166
- blockNumber: z.null(),
167
- blockTimestamp: z.optional(z.null()),
168
- transactionIndex: z.null(),
169
- })
178
+ export const PendingEip8141 = z
179
+ .object({
180
+ ...eip8141Fields(z_Uint.Uint, chainId, z_Frame.Frame, nonceKeys),
181
+ blockHash: z.null(),
182
+ blockNumber: z.null(),
183
+ blockTimestamp: z.optional(z.null()),
184
+ transactionIndex: z.null(),
185
+ })
186
+ .check(z.refine(validNonce, 'Invalid keyed nonce'))
170
187
 
171
188
  function eip8141Fields<
172
189
  uint extends z.ZodMiniType,
173
190
  num extends z.ZodMiniType,
174
191
  frame extends z.ZodMiniType,
175
- >(uint: uint, num: num, frame: frame) {
192
+ keys extends z.ZodMiniType,
193
+ >(uint: uint, num: num, frame: frame, keys: keys) {
176
194
  return {
177
195
  blobVersionedHashes: z.readonly(z.array(z_Hex.Hex)),
178
196
  chainId: num,
@@ -187,6 +205,7 @@ function eip8141Fields<
187
205
  maxFeePerGas: uint,
188
206
  maxPriorityFeePerGas: uint,
189
207
  nonce: uint,
208
+ nonceKeys: z.optional(keys),
190
209
  signatures: z.readonly(z.array(z_FrameSignature.FrameSignature)),
191
210
  to: z.optional(z.nullable(z_Address.Address)),
192
211
  type: Eip8141Type,