ox 0.14.11 → 0.14.12

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 (38) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/_cjs/erc8021/Attribution.js +7 -1
  3. package/_cjs/erc8021/Attribution.js.map +1 -1
  4. package/_cjs/tempo/KeyAuthorization.js +141 -21
  5. package/_cjs/tempo/KeyAuthorization.js.map +1 -1
  6. package/_cjs/tempo/Period.js +31 -0
  7. package/_cjs/tempo/Period.js.map +1 -0
  8. package/_cjs/tempo/index.js +2 -1
  9. package/_cjs/tempo/index.js.map +1 -1
  10. package/_cjs/version.js +1 -1
  11. package/_esm/erc8021/Attribution.js +7 -1
  12. package/_esm/erc8021/Attribution.js.map +1 -1
  13. package/_esm/tempo/KeyAuthorization.js +150 -22
  14. package/_esm/tempo/KeyAuthorization.js.map +1 -1
  15. package/_esm/tempo/Period.js +92 -0
  16. package/_esm/tempo/Period.js.map +1 -0
  17. package/_esm/tempo/index.js +29 -0
  18. package/_esm/tempo/index.js.map +1 -1
  19. package/_esm/version.js +1 -1
  20. package/_types/erc8021/Attribution.d.ts +2 -0
  21. package/_types/erc8021/Attribution.d.ts.map +1 -1
  22. package/_types/tempo/KeyAuthorization.d.ts +91 -19
  23. package/_types/tempo/KeyAuthorization.d.ts.map +1 -1
  24. package/_types/tempo/Period.d.ts +78 -0
  25. package/_types/tempo/Period.d.ts.map +1 -0
  26. package/_types/tempo/index.d.ts +29 -0
  27. package/_types/tempo/index.d.ts.map +1 -1
  28. package/_types/version.d.ts +1 -1
  29. package/erc8021/Attribution.ts +12 -1
  30. package/package.json +6 -1
  31. package/tempo/KeyAuthorization.test.ts +312 -3
  32. package/tempo/KeyAuthorization.ts +277 -51
  33. package/tempo/Period/package.json +6 -0
  34. package/tempo/Period.test.ts +44 -0
  35. package/tempo/Period.ts +97 -0
  36. package/tempo/e2e.test.ts +890 -1
  37. package/tempo/index.ts +30 -0
  38. package/version.ts +1 -1
package/tempo/e2e.test.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import {
2
+ AbiFunction,
2
3
  Address,
3
4
  Hex,
4
5
  P256,
@@ -9,10 +10,11 @@ import {
9
10
  } from 'ox'
10
11
  import { getTransactionCount } from 'viem/actions'
11
12
  import { beforeEach, describe, expect, test } from 'vitest'
12
- import { chain, client, fundAddress } from '../../test/tempo/config.js'
13
+ import { chain, client, fundAddress, nodeEnv } from '../../test/tempo/config.js'
13
14
  import {
14
15
  AuthorizationTempo,
15
16
  KeyAuthorization,
17
+ Period,
16
18
  SignatureEnvelope,
17
19
  } from './index.js'
18
20
  import * as Transaction from './Transaction.js'
@@ -1680,4 +1682,891 @@ describe('behavior: keyAuthorization', () => {
1680
1682
  expect(response.keyAuthorization?.limits).toBeUndefined()
1681
1683
  }
1682
1684
  })
1685
+
1686
+ // TODO: remove skipIf when testnet has T3
1687
+ test.skipIf(nodeEnv === 'testnet')(
1688
+ 'behavior: access key with periodic spending limit',
1689
+ async () => {
1690
+ const privateKey = Secp256k1.randomPrivateKey()
1691
+ const publicKey = Secp256k1.getPublicKey({ privateKey })
1692
+ const address = Address.fromPublicKey(publicKey)
1693
+ const access = {
1694
+ address,
1695
+ publicKey,
1696
+ privateKey,
1697
+ } as const
1698
+
1699
+ const keyAuth = KeyAuthorization.from({
1700
+ address: access.address,
1701
+ chainId: BigInt(chainId),
1702
+ type: 'secp256k1',
1703
+ limits: [
1704
+ {
1705
+ token: '0x20c0000000000000000000000000000000000001',
1706
+ limit: Value.from('1000', 6),
1707
+ period: Period.months(1),
1708
+ },
1709
+ ],
1710
+ })
1711
+
1712
+ const keyAuth_signature = Secp256k1.sign({
1713
+ payload: KeyAuthorization.getSignPayload(keyAuth),
1714
+ privateKey: root.privateKey,
1715
+ })
1716
+
1717
+ const keyAuth_signed = KeyAuthorization.from(keyAuth, {
1718
+ signature: SignatureEnvelope.from(keyAuth_signature),
1719
+ })
1720
+
1721
+ const nonce = await getTransactionCount(client, {
1722
+ address: root.address,
1723
+ blockTag: 'pending',
1724
+ })
1725
+
1726
+ const transaction = TxEnvelopeTempo.from({
1727
+ calls: [
1728
+ {
1729
+ to: '0x0000000000000000000000000000000000000000',
1730
+ },
1731
+ ],
1732
+ chainId,
1733
+ feeToken: '0x20c0000000000000000000000000000000000001',
1734
+ keyAuthorization: keyAuth_signed,
1735
+ nonce: BigInt(nonce),
1736
+ gas: 1_000_000n,
1737
+ maxFeePerGas: Value.fromGwei('20'),
1738
+ maxPriorityFeePerGas: Value.fromGwei('10'),
1739
+ })
1740
+
1741
+ const signature = Secp256k1.sign({
1742
+ payload: TxEnvelopeTempo.getSignPayload(transaction, {
1743
+ from: root.address,
1744
+ }),
1745
+ privateKey: access.privateKey,
1746
+ })
1747
+
1748
+ const serialized_signed = TxEnvelopeTempo.serialize(transaction, {
1749
+ signature: SignatureEnvelope.from({
1750
+ userAddress: root.address,
1751
+ inner: SignatureEnvelope.from(signature),
1752
+ type: 'keychain',
1753
+ }),
1754
+ })
1755
+
1756
+ const receipt = (await client
1757
+ .request({
1758
+ method: 'eth_sendRawTransactionSync',
1759
+ params: [serialized_signed],
1760
+ })
1761
+ .then((tx) => TransactionReceipt.fromRpc(tx as any)))!
1762
+ expect(receipt).toBeDefined()
1763
+ expect(receipt.status).toBe('success')
1764
+
1765
+ {
1766
+ const response = await client
1767
+ .request({
1768
+ method: 'eth_getTransactionByHash',
1769
+ params: [receipt.transactionHash],
1770
+ })
1771
+ .then((tx) => Transaction.fromRpc(tx as any))
1772
+ if (!response) throw new Error()
1773
+
1774
+ expect(response.from).toBe(root.address)
1775
+ expect(response.keyAuthorization).toBeDefined()
1776
+ expect(response.keyAuthorization?.limits?.[0]?.limit).toBe(
1777
+ Value.from('1000', 6),
1778
+ )
1779
+ expect(response.keyAuthorization?.limits?.[0]?.period).toBe(2592000)
1780
+ }
1781
+ },
1782
+ )
1783
+
1784
+ // TODO: remove skipIf when testnet has T3
1785
+ test.skipIf(nodeEnv === 'testnet')(
1786
+ 'behavior: rejects transfer exceeding periodic spending limit',
1787
+ async () => {
1788
+ const privateKey = Secp256k1.randomPrivateKey()
1789
+ const publicKey = Secp256k1.getPublicKey({ privateKey })
1790
+ const address = Address.fromPublicKey(publicKey)
1791
+ const token = '0x20c0000000000000000000000000000000000001'
1792
+ const transfer = AbiFunction.from(
1793
+ 'function transfer(address to, uint256 amount)',
1794
+ )
1795
+
1796
+ // Key with a 5 USDC periodic limit
1797
+ const keyAuth = KeyAuthorization.from({
1798
+ address,
1799
+ chainId: BigInt(chainId),
1800
+ type: 'secp256k1',
1801
+ limits: [
1802
+ {
1803
+ token,
1804
+ limit: Value.from('5', 6),
1805
+ period: Period.months(1),
1806
+ },
1807
+ ],
1808
+ })
1809
+
1810
+ const keyAuth_signed = KeyAuthorization.from(keyAuth, {
1811
+ signature: SignatureEnvelope.from(
1812
+ Secp256k1.sign({
1813
+ payload: KeyAuthorization.getSignPayload(keyAuth),
1814
+ privateKey: root.privateKey,
1815
+ }),
1816
+ ),
1817
+ })
1818
+
1819
+ const nonce = await getTransactionCount(client, {
1820
+ address: root.address,
1821
+ blockTag: 'pending',
1822
+ })
1823
+
1824
+ // Try to transfer 10 USDC (exceeds 5 USDC limit)
1825
+ const transferData = AbiFunction.encodeData(transfer, [
1826
+ '0x0000000000000000000000000000000000000001',
1827
+ Value.from('10', 6),
1828
+ ])
1829
+
1830
+ const transaction = TxEnvelopeTempo.from({
1831
+ calls: [{ to: token, data: transferData }],
1832
+ chainId,
1833
+ feeToken: token,
1834
+ keyAuthorization: keyAuth_signed,
1835
+ nonce: BigInt(nonce),
1836
+ gas: 5_000_000n,
1837
+ maxFeePerGas: Value.fromGwei('20'),
1838
+ maxPriorityFeePerGas: Value.fromGwei('10'),
1839
+ })
1840
+
1841
+ const signature = Secp256k1.sign({
1842
+ payload: TxEnvelopeTempo.getSignPayload(transaction, {
1843
+ from: root.address,
1844
+ }),
1845
+ privateKey,
1846
+ })
1847
+
1848
+ const serialized_signed = TxEnvelopeTempo.serialize(transaction, {
1849
+ signature: SignatureEnvelope.from({
1850
+ userAddress: root.address,
1851
+ inner: SignatureEnvelope.from(signature),
1852
+ type: 'keychain',
1853
+ }),
1854
+ })
1855
+
1856
+ const receipt = (await client
1857
+ .request({
1858
+ method: 'eth_sendRawTransactionSync',
1859
+ params: [serialized_signed],
1860
+ })
1861
+ .then((tx) => TransactionReceipt.fromRpc(tx as any)))!
1862
+ expect(receipt).toBeDefined()
1863
+ expect(receipt.status).toBe('reverted')
1864
+ },
1865
+ )
1866
+
1867
+ test.runIf(nodeEnv === 'localnet')(
1868
+ 'behavior: periodic spending limit resets after period',
1869
+ async () => {
1870
+ const accessPrivateKey = Secp256k1.randomPrivateKey()
1871
+ const accessAddress = Address.fromPublicKey(
1872
+ Secp256k1.getPublicKey({ privateKey: accessPrivateKey }),
1873
+ )
1874
+ const token = '0x20c0000000000000000000000000000000000001'
1875
+ const transfer = AbiFunction.from(
1876
+ 'function transfer(address to, uint256 amount)',
1877
+ )
1878
+ const recipient = '0x0000000000000000000000000000000000000001'
1879
+
1880
+ // Key with a 5 USDC limit that resets every 5 seconds
1881
+ const keyAuth = KeyAuthorization.from({
1882
+ address: accessAddress,
1883
+ chainId: BigInt(chainId),
1884
+ type: 'secp256k1',
1885
+ limits: [
1886
+ {
1887
+ token,
1888
+ limit: Value.from('5', 6),
1889
+ period: Period.seconds(5),
1890
+ },
1891
+ ],
1892
+ })
1893
+
1894
+ const keyAuth_signed = KeyAuthorization.from(keyAuth, {
1895
+ signature: SignatureEnvelope.from(
1896
+ Secp256k1.sign({
1897
+ payload: KeyAuthorization.getSignPayload(keyAuth),
1898
+ privateKey: root.privateKey,
1899
+ }),
1900
+ ),
1901
+ })
1902
+
1903
+ // 1. Provision key + transfer 4 USDC
1904
+ {
1905
+ const nonce = await getTransactionCount(client, {
1906
+ address: root.address,
1907
+ blockTag: 'pending',
1908
+ })
1909
+
1910
+ const transferData = AbiFunction.encodeData(transfer, [
1911
+ recipient,
1912
+ Value.from('4', 6),
1913
+ ])
1914
+
1915
+ const transaction = TxEnvelopeTempo.from({
1916
+ calls: [{ to: token, data: transferData }],
1917
+ chainId,
1918
+ feeToken: token,
1919
+ keyAuthorization: keyAuth_signed,
1920
+ nonce: BigInt(nonce),
1921
+ gas: 5_000_000n,
1922
+ maxFeePerGas: Value.fromGwei('20'),
1923
+ maxPriorityFeePerGas: Value.fromGwei('10'),
1924
+ })
1925
+
1926
+ const signature = Secp256k1.sign({
1927
+ payload: TxEnvelopeTempo.getSignPayload(transaction, {
1928
+ from: root.address,
1929
+ }),
1930
+ privateKey: accessPrivateKey,
1931
+ })
1932
+
1933
+ const serialized = TxEnvelopeTempo.serialize(transaction, {
1934
+ signature: SignatureEnvelope.from({
1935
+ userAddress: root.address,
1936
+ inner: SignatureEnvelope.from(signature),
1937
+ type: 'keychain',
1938
+ }),
1939
+ })
1940
+
1941
+ const receipt = (await client
1942
+ .request({
1943
+ method: 'eth_sendRawTransactionSync',
1944
+ params: [serialized],
1945
+ })
1946
+ .then((tx) => TransactionReceipt.fromRpc(tx as any)))!
1947
+ expect(receipt.status).toBe('success')
1948
+ }
1949
+
1950
+ // 2. Immediately try another 4 USDC transfer (should revert — limit exhausted)
1951
+ {
1952
+ const nonce = await getTransactionCount(client, {
1953
+ address: root.address,
1954
+ blockTag: 'pending',
1955
+ })
1956
+
1957
+ const transferData = AbiFunction.encodeData(transfer, [
1958
+ recipient,
1959
+ Value.from('4', 6),
1960
+ ])
1961
+
1962
+ const transaction = TxEnvelopeTempo.from({
1963
+ calls: [{ to: token, data: transferData }],
1964
+ chainId,
1965
+ feeToken: token,
1966
+ nonce: BigInt(nonce),
1967
+ gas: 5_000_000n,
1968
+ maxFeePerGas: Value.fromGwei('20'),
1969
+ maxPriorityFeePerGas: Value.fromGwei('10'),
1970
+ })
1971
+
1972
+ const signature = Secp256k1.sign({
1973
+ payload: TxEnvelopeTempo.getSignPayload(transaction, {
1974
+ from: root.address,
1975
+ }),
1976
+ privateKey: accessPrivateKey,
1977
+ })
1978
+
1979
+ const serialized = TxEnvelopeTempo.serialize(transaction, {
1980
+ signature: SignatureEnvelope.from({
1981
+ userAddress: root.address,
1982
+ inner: SignatureEnvelope.from(signature),
1983
+ type: 'keychain',
1984
+ }),
1985
+ })
1986
+
1987
+ const receipt = (await client
1988
+ .request({
1989
+ method: 'eth_sendRawTransactionSync',
1990
+ params: [serialized],
1991
+ })
1992
+ .then((tx) => TransactionReceipt.fromRpc(tx as any)))!
1993
+ expect(receipt.status).toBe('reverted')
1994
+ }
1995
+
1996
+ // 3. Wait for period to reset
1997
+ await new Promise((resolve) => setTimeout(resolve, 6000))
1998
+
1999
+ // 4. Transfer 4 USDC again (should succeed — period reset)
2000
+ {
2001
+ const nonce = await getTransactionCount(client, {
2002
+ address: root.address,
2003
+ blockTag: 'pending',
2004
+ })
2005
+
2006
+ const transferData = AbiFunction.encodeData(transfer, [
2007
+ recipient,
2008
+ Value.from('4', 6),
2009
+ ])
2010
+
2011
+ const transaction = TxEnvelopeTempo.from({
2012
+ calls: [{ to: token, data: transferData }],
2013
+ chainId,
2014
+ feeToken: token,
2015
+ nonce: BigInt(nonce),
2016
+ gas: 5_000_000n,
2017
+ maxFeePerGas: Value.fromGwei('20'),
2018
+ maxPriorityFeePerGas: Value.fromGwei('10'),
2019
+ })
2020
+
2021
+ const signature = Secp256k1.sign({
2022
+ payload: TxEnvelopeTempo.getSignPayload(transaction, {
2023
+ from: root.address,
2024
+ }),
2025
+ privateKey: accessPrivateKey,
2026
+ })
2027
+
2028
+ const serialized = TxEnvelopeTempo.serialize(transaction, {
2029
+ signature: SignatureEnvelope.from({
2030
+ userAddress: root.address,
2031
+ inner: SignatureEnvelope.from(signature),
2032
+ type: 'keychain',
2033
+ }),
2034
+ })
2035
+
2036
+ const receipt = (await client
2037
+ .request({
2038
+ method: 'eth_sendRawTransactionSync',
2039
+ params: [serialized],
2040
+ })
2041
+ .then((tx) => TransactionReceipt.fromRpc(tx as any)))!
2042
+ expect(receipt.status).toBe('success')
2043
+ }
2044
+ },
2045
+ )
2046
+
2047
+ // TODO: remove skipIf when testnet has T3
2048
+ test.skipIf(nodeEnv === 'testnet')(
2049
+ 'behavior: access key with call scopes (transfer)',
2050
+ async () => {
2051
+ const accessPrivateKey = Secp256k1.randomPrivateKey()
2052
+ const accessAddress = Address.fromPublicKey(
2053
+ Secp256k1.getPublicKey({ privateKey: accessPrivateKey }),
2054
+ )
2055
+ const recipient = '0x0000000000000000000000000000000000000001'
2056
+ const token = '0x20c0000000000000000000000000000000000001'
2057
+ const transfer = AbiFunction.from(
2058
+ 'function transfer(address to, uint256 amount)',
2059
+ )
2060
+ const transferData = AbiFunction.encodeData(transfer, [
2061
+ recipient,
2062
+ Value.from('1', 6),
2063
+ ])
2064
+
2065
+ // Scope key: only transfer() on token contract, with sufficient spending limit
2066
+ const keyAuth = KeyAuthorization.from({
2067
+ address: accessAddress,
2068
+ chainId: BigInt(chainId),
2069
+ type: 'secp256k1',
2070
+ limits: [{ token, limit: Value.from('10000', 6) }],
2071
+ scopes: [
2072
+ {
2073
+ contractAddress: token,
2074
+ selector: AbiFunction.getSelector(transfer),
2075
+ },
2076
+ ],
2077
+ })
2078
+
2079
+ const keyAuth_signed = KeyAuthorization.from(keyAuth, {
2080
+ signature: SignatureEnvelope.from(
2081
+ Secp256k1.sign({
2082
+ payload: KeyAuthorization.getSignPayload(keyAuth),
2083
+ privateKey: root.privateKey,
2084
+ }),
2085
+ ),
2086
+ })
2087
+
2088
+ const nonce = await getTransactionCount(client, {
2089
+ address: root.address,
2090
+ blockTag: 'pending',
2091
+ })
2092
+
2093
+ const transaction = TxEnvelopeTempo.from({
2094
+ calls: [{ to: token, data: transferData }],
2095
+ chainId,
2096
+ feeToken: token,
2097
+ keyAuthorization: keyAuth_signed,
2098
+ nonce: BigInt(nonce),
2099
+ gas: 5_000_000n,
2100
+ maxFeePerGas: Value.fromGwei('20'),
2101
+ maxPriorityFeePerGas: Value.fromGwei('10'),
2102
+ })
2103
+
2104
+ const signature = Secp256k1.sign({
2105
+ payload: TxEnvelopeTempo.getSignPayload(transaction, {
2106
+ from: root.address,
2107
+ }),
2108
+ privateKey: accessPrivateKey,
2109
+ })
2110
+
2111
+ const serialized_signed = TxEnvelopeTempo.serialize(transaction, {
2112
+ signature: SignatureEnvelope.from({
2113
+ userAddress: root.address,
2114
+ inner: SignatureEnvelope.from(signature),
2115
+ type: 'keychain',
2116
+ }),
2117
+ })
2118
+
2119
+ const receipt = (await client
2120
+ .request({
2121
+ method: 'eth_sendRawTransactionSync',
2122
+ params: [serialized_signed],
2123
+ })
2124
+ .then((tx) => TransactionReceipt.fromRpc(tx as any)))!
2125
+ expect(receipt).toBeDefined()
2126
+ expect(receipt.status).toBe('success')
2127
+
2128
+ {
2129
+ const response = await client
2130
+ .request({
2131
+ method: 'eth_getTransactionByHash',
2132
+ params: [receipt.transactionHash],
2133
+ })
2134
+ .then((tx) => Transaction.fromRpc(tx as any))
2135
+ if (!response) throw new Error()
2136
+
2137
+ expect(response.from).toBe(root.address)
2138
+ expect(response.keyAuthorization).toBeDefined()
2139
+ expect(response.keyAuthorization?.scopes?.[0]?.contractAddress).toBe(
2140
+ token,
2141
+ )
2142
+ expect(response.keyAuthorization?.scopes?.[0]?.selector).toBe(
2143
+ '0xa9059cbb',
2144
+ )
2145
+ }
2146
+ },
2147
+ )
2148
+
2149
+ // TODO: remove skipIf when testnet has T3
2150
+ test.skipIf(nodeEnv === 'testnet')(
2151
+ 'behavior: access key with call scopes + recipient allowlist (transfer)',
2152
+ async () => {
2153
+ const accessPrivateKey = Secp256k1.randomPrivateKey()
2154
+ const accessAddress = Address.fromPublicKey(
2155
+ Secp256k1.getPublicKey({ privateKey: accessPrivateKey }),
2156
+ )
2157
+ const recipient = '0x0000000000000000000000000000000000000001'
2158
+ const token = '0x20c0000000000000000000000000000000000001'
2159
+ const transfer = AbiFunction.from(
2160
+ 'function transfer(address to, uint256 amount)',
2161
+ )
2162
+ const transferData = AbiFunction.encodeData(transfer, [
2163
+ recipient,
2164
+ Value.from('1', 6),
2165
+ ])
2166
+
2167
+ // Scope key: transfer() on token, only to recipient, with sufficient spending limit
2168
+ const keyAuth = KeyAuthorization.from({
2169
+ address: accessAddress,
2170
+ chainId: BigInt(chainId),
2171
+ type: 'secp256k1',
2172
+ limits: [{ token, limit: Value.from('10000', 6) }],
2173
+ scopes: [
2174
+ {
2175
+ contractAddress: token,
2176
+ selector: AbiFunction.getSelector(transfer),
2177
+ recipients: [recipient],
2178
+ },
2179
+ ],
2180
+ })
2181
+
2182
+ const keyAuth_signed = KeyAuthorization.from(keyAuth, {
2183
+ signature: SignatureEnvelope.from(
2184
+ Secp256k1.sign({
2185
+ payload: KeyAuthorization.getSignPayload(keyAuth),
2186
+ privateKey: root.privateKey,
2187
+ }),
2188
+ ),
2189
+ })
2190
+
2191
+ const nonce = await getTransactionCount(client, {
2192
+ address: root.address,
2193
+ blockTag: 'pending',
2194
+ })
2195
+
2196
+ const transaction = TxEnvelopeTempo.from({
2197
+ calls: [{ to: token, data: transferData }],
2198
+ chainId,
2199
+ feeToken: token,
2200
+ keyAuthorization: keyAuth_signed,
2201
+ nonce: BigInt(nonce),
2202
+ gas: 5_000_000n,
2203
+ maxFeePerGas: Value.fromGwei('20'),
2204
+ maxPriorityFeePerGas: Value.fromGwei('10'),
2205
+ })
2206
+
2207
+ const signature = Secp256k1.sign({
2208
+ payload: TxEnvelopeTempo.getSignPayload(transaction, {
2209
+ from: root.address,
2210
+ }),
2211
+ privateKey: accessPrivateKey,
2212
+ })
2213
+
2214
+ const serialized_signed = TxEnvelopeTempo.serialize(transaction, {
2215
+ signature: SignatureEnvelope.from({
2216
+ userAddress: root.address,
2217
+ inner: SignatureEnvelope.from(signature),
2218
+ type: 'keychain',
2219
+ }),
2220
+ })
2221
+
2222
+ const receipt = (await client
2223
+ .request({
2224
+ method: 'eth_sendRawTransactionSync',
2225
+ params: [serialized_signed],
2226
+ })
2227
+ .then((tx) => TransactionReceipt.fromRpc(tx as any)))!
2228
+ expect(receipt).toBeDefined()
2229
+ expect(receipt.status).toBe('success')
2230
+
2231
+ {
2232
+ const response = await client
2233
+ .request({
2234
+ method: 'eth_getTransactionByHash',
2235
+ params: [receipt.transactionHash],
2236
+ })
2237
+ .then((tx) => Transaction.fromRpc(tx as any))
2238
+ if (!response) throw new Error()
2239
+
2240
+ expect(response.from).toBe(root.address)
2241
+ expect(response.keyAuthorization).toBeDefined()
2242
+ expect(response.keyAuthorization?.scopes?.[0]?.selector).toBe(
2243
+ '0xa9059cbb',
2244
+ )
2245
+ expect(response.keyAuthorization?.scopes?.[0]?.recipients).toEqual([
2246
+ recipient,
2247
+ ])
2248
+ }
2249
+ },
2250
+ )
2251
+
2252
+ // TODO: remove skipIf when testnet has T3
2253
+ test.skipIf(nodeEnv === 'testnet')(
2254
+ 'behavior: rejects transfer to wrong contract (outside scope)',
2255
+ async () => {
2256
+ const accessPrivateKey = Secp256k1.randomPrivateKey()
2257
+ const accessAddress = Address.fromPublicKey(
2258
+ Secp256k1.getPublicKey({ privateKey: accessPrivateKey }),
2259
+ )
2260
+ const token1 = '0x20c0000000000000000000000000000000000001'
2261
+ const token2 = '0x20c0000000000000000000000000000000000002'
2262
+ const transfer = AbiFunction.from(
2263
+ 'function transfer(address to, uint256 amount)',
2264
+ )
2265
+ const transferData = AbiFunction.encodeData(transfer, [
2266
+ '0x0000000000000000000000000000000000000001',
2267
+ Value.from('1', 6),
2268
+ ])
2269
+
2270
+ // Scope key to only token1
2271
+ const keyAuth = KeyAuthorization.from({
2272
+ address: accessAddress,
2273
+ chainId: BigInt(chainId),
2274
+ type: 'secp256k1',
2275
+ scopes: [
2276
+ {
2277
+ contractAddress: token1,
2278
+ selector: AbiFunction.getSelector(transfer),
2279
+ },
2280
+ ],
2281
+ })
2282
+
2283
+ const keyAuth_signed = KeyAuthorization.from(keyAuth, {
2284
+ signature: SignatureEnvelope.from(
2285
+ Secp256k1.sign({
2286
+ payload: KeyAuthorization.getSignPayload(keyAuth),
2287
+ privateKey: root.privateKey,
2288
+ }),
2289
+ ),
2290
+ })
2291
+
2292
+ const nonce = await getTransactionCount(client, {
2293
+ address: root.address,
2294
+ blockTag: 'pending',
2295
+ })
2296
+
2297
+ // Call transfer on token2 (not scoped) — should be rejected
2298
+ const transaction = TxEnvelopeTempo.from({
2299
+ calls: [{ to: token2, data: transferData }],
2300
+ chainId,
2301
+ feeToken: token1,
2302
+ keyAuthorization: keyAuth_signed,
2303
+ nonce: BigInt(nonce),
2304
+ gas: 5_000_000n,
2305
+ maxFeePerGas: Value.fromGwei('20'),
2306
+ maxPriorityFeePerGas: Value.fromGwei('10'),
2307
+ })
2308
+
2309
+ const signature = Secp256k1.sign({
2310
+ payload: TxEnvelopeTempo.getSignPayload(transaction, {
2311
+ from: root.address,
2312
+ }),
2313
+ privateKey: accessPrivateKey,
2314
+ })
2315
+
2316
+ const serialized_signed = TxEnvelopeTempo.serialize(transaction, {
2317
+ signature: SignatureEnvelope.from({
2318
+ userAddress: root.address,
2319
+ inner: SignatureEnvelope.from(signature),
2320
+ type: 'keychain',
2321
+ }),
2322
+ })
2323
+
2324
+ await expect(
2325
+ client.request({
2326
+ method: 'eth_sendRawTransactionSync',
2327
+ params: [serialized_signed],
2328
+ }),
2329
+ ).rejects.toThrow()
2330
+ },
2331
+ )
2332
+
2333
+ // TODO: remove skipIf when testnet has T3
2334
+ test.skipIf(nodeEnv === 'testnet')(
2335
+ 'behavior: rejects approve when only transfer is scoped',
2336
+ async () => {
2337
+ const accessPrivateKey = Secp256k1.randomPrivateKey()
2338
+ const accessAddress = Address.fromPublicKey(
2339
+ Secp256k1.getPublicKey({ privateKey: accessPrivateKey }),
2340
+ )
2341
+ const token = '0x20c0000000000000000000000000000000000001'
2342
+ const transfer = AbiFunction.from(
2343
+ 'function transfer(address to, uint256 amount)',
2344
+ )
2345
+ const approve = AbiFunction.from(
2346
+ 'function approve(address spender, uint256 amount)',
2347
+ )
2348
+ const approveData = AbiFunction.encodeData(approve, [
2349
+ '0x0000000000000000000000000000000000000001',
2350
+ Value.from('1', 6),
2351
+ ])
2352
+
2353
+ // Scope key to only transfer()
2354
+ const keyAuth = KeyAuthorization.from({
2355
+ address: accessAddress,
2356
+ chainId: BigInt(chainId),
2357
+ type: 'secp256k1',
2358
+ scopes: [
2359
+ {
2360
+ contractAddress: token,
2361
+ selector: AbiFunction.getSelector(transfer),
2362
+ },
2363
+ ],
2364
+ })
2365
+
2366
+ const keyAuth_signed = KeyAuthorization.from(keyAuth, {
2367
+ signature: SignatureEnvelope.from(
2368
+ Secp256k1.sign({
2369
+ payload: KeyAuthorization.getSignPayload(keyAuth),
2370
+ privateKey: root.privateKey,
2371
+ }),
2372
+ ),
2373
+ })
2374
+
2375
+ const nonce = await getTransactionCount(client, {
2376
+ address: root.address,
2377
+ blockTag: 'pending',
2378
+ })
2379
+
2380
+ // Call approve() instead — should be rejected
2381
+ const transaction = TxEnvelopeTempo.from({
2382
+ calls: [{ to: token, data: approveData }],
2383
+ chainId,
2384
+ feeToken: token,
2385
+ keyAuthorization: keyAuth_signed,
2386
+ nonce: BigInt(nonce),
2387
+ gas: 5_000_000n,
2388
+ maxFeePerGas: Value.fromGwei('20'),
2389
+ maxPriorityFeePerGas: Value.fromGwei('10'),
2390
+ })
2391
+
2392
+ const signature = Secp256k1.sign({
2393
+ payload: TxEnvelopeTempo.getSignPayload(transaction, {
2394
+ from: root.address,
2395
+ }),
2396
+ privateKey: accessPrivateKey,
2397
+ })
2398
+
2399
+ const serialized_signed = TxEnvelopeTempo.serialize(transaction, {
2400
+ signature: SignatureEnvelope.from({
2401
+ userAddress: root.address,
2402
+ inner: SignatureEnvelope.from(signature),
2403
+ type: 'keychain',
2404
+ }),
2405
+ })
2406
+
2407
+ await expect(
2408
+ client.request({
2409
+ method: 'eth_sendRawTransactionSync',
2410
+ params: [serialized_signed],
2411
+ }),
2412
+ ).rejects.toThrow()
2413
+ },
2414
+ )
2415
+
2416
+ // TODO: remove skipIf when testnet has T3
2417
+ test.skipIf(nodeEnv === 'testnet')(
2418
+ 'behavior: rejects transfer to wrong recipient',
2419
+ async () => {
2420
+ const accessPrivateKey = Secp256k1.randomPrivateKey()
2421
+ const accessAddress = Address.fromPublicKey(
2422
+ Secp256k1.getPublicKey({ privateKey: accessPrivateKey }),
2423
+ )
2424
+ const token = '0x20c0000000000000000000000000000000000001'
2425
+ const allowedRecipient = '0x0000000000000000000000000000000000000001'
2426
+ const wrongRecipient = '0x0000000000000000000000000000000000000002'
2427
+ const transfer = AbiFunction.from(
2428
+ 'function transfer(address to, uint256 amount)',
2429
+ )
2430
+ const transferData = AbiFunction.encodeData(transfer, [
2431
+ wrongRecipient,
2432
+ Value.from('1', 6),
2433
+ ])
2434
+
2435
+ // Scope key: transfer only to allowedRecipient
2436
+ const keyAuth = KeyAuthorization.from({
2437
+ address: accessAddress,
2438
+ chainId: BigInt(chainId),
2439
+ type: 'secp256k1',
2440
+ scopes: [
2441
+ {
2442
+ contractAddress: token,
2443
+ selector: AbiFunction.getSelector(transfer),
2444
+ recipients: [allowedRecipient],
2445
+ },
2446
+ ],
2447
+ })
2448
+
2449
+ const keyAuth_signed = KeyAuthorization.from(keyAuth, {
2450
+ signature: SignatureEnvelope.from(
2451
+ Secp256k1.sign({
2452
+ payload: KeyAuthorization.getSignPayload(keyAuth),
2453
+ privateKey: root.privateKey,
2454
+ }),
2455
+ ),
2456
+ })
2457
+
2458
+ const nonce = await getTransactionCount(client, {
2459
+ address: root.address,
2460
+ blockTag: 'pending',
2461
+ })
2462
+
2463
+ // transfer to wrongRecipient — should be rejected
2464
+ const transaction = TxEnvelopeTempo.from({
2465
+ calls: [{ to: token, data: transferData }],
2466
+ chainId,
2467
+ feeToken: token,
2468
+ keyAuthorization: keyAuth_signed,
2469
+ nonce: BigInt(nonce),
2470
+ gas: 5_000_000n,
2471
+ maxFeePerGas: Value.fromGwei('20'),
2472
+ maxPriorityFeePerGas: Value.fromGwei('10'),
2473
+ })
2474
+
2475
+ const signature = Secp256k1.sign({
2476
+ payload: TxEnvelopeTempo.getSignPayload(transaction, {
2477
+ from: root.address,
2478
+ }),
2479
+ privateKey: accessPrivateKey,
2480
+ })
2481
+
2482
+ const serialized_signed = TxEnvelopeTempo.serialize(transaction, {
2483
+ signature: SignatureEnvelope.from({
2484
+ userAddress: root.address,
2485
+ inner: SignatureEnvelope.from(signature),
2486
+ type: 'keychain',
2487
+ }),
2488
+ })
2489
+
2490
+ await expect(
2491
+ client.request({
2492
+ method: 'eth_sendRawTransactionSync',
2493
+ params: [serialized_signed],
2494
+ }),
2495
+ ).rejects.toThrow()
2496
+ },
2497
+ )
2498
+
2499
+ // TODO: remove skipIf when testnet has T3
2500
+ test.skipIf(nodeEnv === 'testnet')(
2501
+ 'behavior: rejects any call when scopes = [] (empty)',
2502
+ async () => {
2503
+ const accessPrivateKey = Secp256k1.randomPrivateKey()
2504
+ const accessAddress = Address.fromPublicKey(
2505
+ Secp256k1.getPublicKey({ privateKey: accessPrivateKey }),
2506
+ )
2507
+ const token = '0x20c0000000000000000000000000000000000001'
2508
+ const transfer = AbiFunction.from(
2509
+ 'function transfer(address to, uint256 amount)',
2510
+ )
2511
+ const transferData = AbiFunction.encodeData(transfer, [
2512
+ '0x0000000000000000000000000000000000000001',
2513
+ Value.from('1', 6),
2514
+ ])
2515
+
2516
+ // scopes = [] → scoped mode with NO calls allowed
2517
+ const keyAuth = KeyAuthorization.from({
2518
+ address: accessAddress,
2519
+ chainId: BigInt(chainId),
2520
+ type: 'secp256k1',
2521
+ scopes: [],
2522
+ })
2523
+
2524
+ const keyAuth_signed = KeyAuthorization.from(keyAuth, {
2525
+ signature: SignatureEnvelope.from(
2526
+ Secp256k1.sign({
2527
+ payload: KeyAuthorization.getSignPayload(keyAuth),
2528
+ privateKey: root.privateKey,
2529
+ }),
2530
+ ),
2531
+ })
2532
+
2533
+ const nonce = await getTransactionCount(client, {
2534
+ address: root.address,
2535
+ blockTag: 'pending',
2536
+ })
2537
+
2538
+ const transaction = TxEnvelopeTempo.from({
2539
+ calls: [{ to: token, data: transferData }],
2540
+ chainId,
2541
+ feeToken: token,
2542
+ keyAuthorization: keyAuth_signed,
2543
+ nonce: BigInt(nonce),
2544
+ gas: 5_000_000n,
2545
+ maxFeePerGas: Value.fromGwei('20'),
2546
+ maxPriorityFeePerGas: Value.fromGwei('10'),
2547
+ })
2548
+
2549
+ const signature = Secp256k1.sign({
2550
+ payload: TxEnvelopeTempo.getSignPayload(transaction, {
2551
+ from: root.address,
2552
+ }),
2553
+ privateKey: accessPrivateKey,
2554
+ })
2555
+
2556
+ const serialized_signed = TxEnvelopeTempo.serialize(transaction, {
2557
+ signature: SignatureEnvelope.from({
2558
+ userAddress: root.address,
2559
+ inner: SignatureEnvelope.from(signature),
2560
+ type: 'keychain',
2561
+ }),
2562
+ })
2563
+
2564
+ await expect(
2565
+ client.request({
2566
+ method: 'eth_sendRawTransactionSync',
2567
+ params: [serialized_signed],
2568
+ }),
2569
+ ).rejects.toThrow()
2570
+ },
2571
+ )
1683
2572
  })