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.
- package/CHANGELOG.md +18 -0
- package/dist/core/Transaction.d.ts +5 -2
- package/dist/core/Transaction.d.ts.map +1 -1
- package/dist/core/Transaction.js +8 -0
- package/dist/core/Transaction.js.map +1 -1
- package/dist/core/TransactionRequest.d.ts +5 -2
- package/dist/core/TransactionRequest.d.ts.map +1 -1
- package/dist/core/TransactionRequest.js +8 -0
- package/dist/core/TransactionRequest.js.map +1 -1
- package/dist/core/TxEnvelope.d.ts +1 -1
- package/dist/core/TxEnvelope.d.ts.map +1 -1
- package/dist/core/TxEnvelope.js +2 -1
- package/dist/core/TxEnvelope.js.map +1 -1
- package/dist/core/TxEnvelopeEip8141.d.ts +18 -7
- package/dist/core/TxEnvelopeEip8141.d.ts.map +1 -1
- package/dist/core/TxEnvelopeEip8141.js +37 -8
- package/dist/core/TxEnvelopeEip8141.js.map +1 -1
- package/dist/core/internal/frameNonce.d.ts +10 -0
- package/dist/core/internal/frameNonce.d.ts.map +1 -0
- package/dist/core/internal/frameNonce.js +45 -0
- package/dist/core/internal/frameNonce.js.map +1 -0
- package/dist/zod/Block.d.ts +3 -0
- package/dist/zod/Block.d.ts.map +1 -1
- package/dist/zod/RpcSchema.d.ts +23 -0
- package/dist/zod/RpcSchema.d.ts.map +1 -1
- package/dist/zod/Transaction.d.ts +6 -0
- package/dist/zod/Transaction.d.ts.map +1 -1
- package/dist/zod/Transaction.js +18 -11
- package/dist/zod/Transaction.js.map +1 -1
- package/dist/zod/TransactionEnvelope.d.ts +10 -0
- package/dist/zod/TransactionEnvelope.d.ts.map +1 -1
- package/dist/zod/TransactionRequest.d.ts +2 -0
- package/dist/zod/TransactionRequest.d.ts.map +1 -1
- package/dist/zod/TransactionRequest.js +9 -3
- package/dist/zod/TransactionRequest.js.map +1 -1
- package/dist/zod/TxEnvelopeEip8141.d.ts +7 -0
- package/dist/zod/TxEnvelopeEip8141.d.ts.map +1 -1
- package/dist/zod/TxEnvelopeEip8141.js +2 -0
- package/dist/zod/TxEnvelopeEip8141.js.map +1 -1
- package/dist/zod/internal/Frame.d.ts +8 -0
- package/dist/zod/internal/Frame.d.ts.map +1 -1
- package/dist/zod/internal/Frame.js +29 -0
- package/dist/zod/internal/Frame.js.map +1 -1
- package/dist/zod/internal/rpcSchemas/Eth.d.ts +23 -0
- package/dist/zod/internal/rpcSchemas/Eth.d.ts.map +1 -1
- package/dist/zod/internal/rpcSchemas/Wallet.d.ts +2 -0
- package/dist/zod/internal/rpcSchemas/Wallet.d.ts.map +1 -1
- package/dist/zod/tempo/RpcSchemaTempo.d.ts +4 -0
- package/dist/zod/tempo/RpcSchemaTempo.d.ts.map +1 -1
- package/dist/zod/tempo/Transaction.d.ts +3 -0
- package/dist/zod/tempo/Transaction.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/core/Transaction.ts +17 -0
- package/src/core/TransactionRequest.ts +12 -0
- package/src/core/TxEnvelope.ts +7 -2
- package/src/core/TxEnvelopeEip8141.ts +61 -13
- package/src/core/_test/TransactionEnvelope.test-d.ts +27 -0
- package/src/core/_test/TransactionEnvelope.test.ts +26 -0
- package/src/core/_test/TransactionRequest.test.ts +28 -0
- package/src/core/_test/TxEnvelopeEip8141.test-d.ts +27 -0
- package/src/core/_test/TxEnvelopeEip8141.test.ts +456 -2
- package/src/core/internal/frameNonce.ts +61 -0
- package/src/version.ts +1 -1
- package/src/zod/Transaction.ts +42 -23
- package/src/zod/TransactionRequest.ts +32 -17
- package/src/zod/TxEnvelopeEip8141.ts +2 -0
- package/src/zod/_test/TxEnvelopeEip8141.nonce.test.ts +174 -0
- package/src/zod/_test/TxEnvelopeEip8141.test-d.ts +29 -0
- 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.
|
|
2
|
+
export const version = '1.8.4'
|
package/src/zod/Transaction.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
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,
|