ox 0.14.11 → 0.14.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.
- package/CHANGELOG.md +14 -0
- package/_cjs/erc8021/Attribution.js +7 -1
- package/_cjs/erc8021/Attribution.js.map +1 -1
- package/_cjs/tempo/KeyAuthorization.js +150 -21
- package/_cjs/tempo/KeyAuthorization.js.map +1 -1
- package/_cjs/tempo/Period.js +31 -0
- package/_cjs/tempo/Period.js.map +1 -0
- package/_cjs/tempo/index.js +2 -1
- package/_cjs/tempo/index.js.map +1 -1
- package/_cjs/version.js +1 -1
- package/_esm/erc8021/Attribution.js +7 -1
- package/_esm/erc8021/Attribution.js.map +1 -1
- package/_esm/tempo/KeyAuthorization.js +159 -22
- package/_esm/tempo/KeyAuthorization.js.map +1 -1
- package/_esm/tempo/Period.js +92 -0
- package/_esm/tempo/Period.js.map +1 -0
- package/_esm/tempo/index.js +29 -0
- package/_esm/tempo/index.js.map +1 -1
- package/_esm/version.js +1 -1
- package/_types/erc8021/Attribution.d.ts +2 -0
- package/_types/erc8021/Attribution.d.ts.map +1 -1
- package/_types/tempo/KeyAuthorization.d.ts +95 -19
- package/_types/tempo/KeyAuthorization.d.ts.map +1 -1
- package/_types/tempo/Period.d.ts +78 -0
- package/_types/tempo/Period.d.ts.map +1 -0
- package/_types/tempo/index.d.ts +29 -0
- package/_types/tempo/index.d.ts.map +1 -1
- package/_types/version.d.ts +1 -1
- package/erc8021/Attribution.ts +12 -1
- package/package.json +6 -1
- package/tempo/KeyAuthorization.test.ts +407 -3
- package/tempo/KeyAuthorization.ts +291 -51
- package/tempo/Period/package.json +6 -0
- package/tempo/Period.test.ts +44 -0
- package/tempo/Period.ts +97 -0
- package/tempo/e2e.test.ts +969 -1
- package/tempo/index.ts +30 -0
- 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'
|
|
@@ -779,6 +781,87 @@ test('behavior: feePayerSignature (user → feePayer)', async () => {
|
|
|
779
781
|
expect(from).toBe(senderAddress)
|
|
780
782
|
})
|
|
781
783
|
|
|
784
|
+
test('behavior: feePayerSignature (feePayer → user)', async () => {
|
|
785
|
+
const userPrivateKey = Secp256k1.randomPrivateKey()
|
|
786
|
+
const userAddress = Address.fromPublicKey(
|
|
787
|
+
Secp256k1.getPublicKey({ privateKey: userPrivateKey }),
|
|
788
|
+
)
|
|
789
|
+
|
|
790
|
+
const feePayerPrivateKey = Secp256k1.randomPrivateKey()
|
|
791
|
+
const feePayerAddress = Address.fromPublicKey(
|
|
792
|
+
Secp256k1.getPublicKey({ privateKey: feePayerPrivateKey }),
|
|
793
|
+
)
|
|
794
|
+
|
|
795
|
+
await Promise.all([
|
|
796
|
+
fundAddress(client, { address: userAddress }),
|
|
797
|
+
fundAddress(client, { address: feePayerAddress }),
|
|
798
|
+
])
|
|
799
|
+
|
|
800
|
+
const nonce = await getTransactionCount(client, {
|
|
801
|
+
address: userAddress,
|
|
802
|
+
blockTag: 'pending',
|
|
803
|
+
})
|
|
804
|
+
|
|
805
|
+
//////////////////////////////////////////////////////////////////
|
|
806
|
+
// Fee payer flow
|
|
807
|
+
|
|
808
|
+
// 1. Build the transaction with `feePayerSignature: null` to indicate
|
|
809
|
+
// fee sponsorship intent. The user does NOT commit to `feeToken`.
|
|
810
|
+
const transaction = TxEnvelopeTempo.from({
|
|
811
|
+
calls: [
|
|
812
|
+
{
|
|
813
|
+
to: '0x0000000000000000000000000000000000000000',
|
|
814
|
+
},
|
|
815
|
+
],
|
|
816
|
+
chainId,
|
|
817
|
+
feePayerSignature: null,
|
|
818
|
+
feeToken: '0x20c0000000000000000000000000000000000001',
|
|
819
|
+
nonce: BigInt(nonce),
|
|
820
|
+
gas: 500_000n,
|
|
821
|
+
maxFeePerGas: Value.fromGwei('20'),
|
|
822
|
+
maxPriorityFeePerGas: Value.fromGwei('10'),
|
|
823
|
+
})
|
|
824
|
+
|
|
825
|
+
// 2. Fee payer signs first — commits to the sender address and fee token.
|
|
826
|
+
const feePayerSignature = Secp256k1.sign({
|
|
827
|
+
payload: TxEnvelopeTempo.getFeePayerSignPayload(transaction, {
|
|
828
|
+
sender: userAddress,
|
|
829
|
+
}),
|
|
830
|
+
privateKey: feePayerPrivateKey,
|
|
831
|
+
})
|
|
832
|
+
|
|
833
|
+
// 3. Attach fee payer signature to the transaction.
|
|
834
|
+
const transaction_feePayer = TxEnvelopeTempo.from(transaction, {
|
|
835
|
+
feePayerSignature,
|
|
836
|
+
})
|
|
837
|
+
|
|
838
|
+
//////////////////////////////////////////////////////////////////
|
|
839
|
+
// User flow
|
|
840
|
+
|
|
841
|
+
// 4. User signs second — `feePayerSignature` presence causes `feeToken`
|
|
842
|
+
// to be skipped from the user's signing payload.
|
|
843
|
+
const userSignature = Secp256k1.sign({
|
|
844
|
+
payload: TxEnvelopeTempo.getSignPayload(transaction_feePayer),
|
|
845
|
+
privateKey: userPrivateKey,
|
|
846
|
+
})
|
|
847
|
+
|
|
848
|
+
// 5. Serialize with both signatures.
|
|
849
|
+
const serialized_signed = TxEnvelopeTempo.serialize(transaction_feePayer, {
|
|
850
|
+
signature: SignatureEnvelope.from(userSignature),
|
|
851
|
+
})
|
|
852
|
+
|
|
853
|
+
const receipt = (await client
|
|
854
|
+
.request({
|
|
855
|
+
method: 'eth_sendRawTransactionSync',
|
|
856
|
+
params: [serialized_signed],
|
|
857
|
+
})
|
|
858
|
+
.then((tx) => TransactionReceipt.fromRpc(tx as any)))!
|
|
859
|
+
expect(receipt).toBeDefined()
|
|
860
|
+
expect(receipt.status).toBe('success')
|
|
861
|
+
expect(receipt.from).toBe(userAddress)
|
|
862
|
+
expect(receipt.feePayer).toBe(feePayerAddress)
|
|
863
|
+
})
|
|
864
|
+
|
|
782
865
|
describe('behavior: keyAuthorization', () => {
|
|
783
866
|
const privateKey = Secp256k1.randomPrivateKey()
|
|
784
867
|
const address = Address.fromPublicKey(Secp256k1.getPublicKey({ privateKey }))
|
|
@@ -1680,4 +1763,889 @@ describe('behavior: keyAuthorization', () => {
|
|
|
1680
1763
|
expect(response.keyAuthorization?.limits).toBeUndefined()
|
|
1681
1764
|
}
|
|
1682
1765
|
})
|
|
1766
|
+
|
|
1767
|
+
// TODO: remove skipIf when testnet has T3
|
|
1768
|
+
test.skipIf(nodeEnv === 'testnet')(
|
|
1769
|
+
'behavior: access key with periodic spending limit',
|
|
1770
|
+
async () => {
|
|
1771
|
+
const privateKey = Secp256k1.randomPrivateKey()
|
|
1772
|
+
const publicKey = Secp256k1.getPublicKey({ privateKey })
|
|
1773
|
+
const address = Address.fromPublicKey(publicKey)
|
|
1774
|
+
const access = {
|
|
1775
|
+
address,
|
|
1776
|
+
publicKey,
|
|
1777
|
+
privateKey,
|
|
1778
|
+
} as const
|
|
1779
|
+
|
|
1780
|
+
const keyAuth = KeyAuthorization.from({
|
|
1781
|
+
address: access.address,
|
|
1782
|
+
chainId: BigInt(chainId),
|
|
1783
|
+
type: 'secp256k1',
|
|
1784
|
+
limits: [
|
|
1785
|
+
{
|
|
1786
|
+
token: '0x20c0000000000000000000000000000000000001',
|
|
1787
|
+
limit: Value.from('1000', 6),
|
|
1788
|
+
period: Period.months(1),
|
|
1789
|
+
},
|
|
1790
|
+
],
|
|
1791
|
+
})
|
|
1792
|
+
|
|
1793
|
+
const keyAuth_signature = Secp256k1.sign({
|
|
1794
|
+
payload: KeyAuthorization.getSignPayload(keyAuth),
|
|
1795
|
+
privateKey: root.privateKey,
|
|
1796
|
+
})
|
|
1797
|
+
|
|
1798
|
+
const keyAuth_signed = KeyAuthorization.from(keyAuth, {
|
|
1799
|
+
signature: SignatureEnvelope.from(keyAuth_signature),
|
|
1800
|
+
})
|
|
1801
|
+
|
|
1802
|
+
const nonce = await getTransactionCount(client, {
|
|
1803
|
+
address: root.address,
|
|
1804
|
+
blockTag: 'pending',
|
|
1805
|
+
})
|
|
1806
|
+
|
|
1807
|
+
const transaction = TxEnvelopeTempo.from({
|
|
1808
|
+
calls: [
|
|
1809
|
+
{
|
|
1810
|
+
to: '0x0000000000000000000000000000000000000000',
|
|
1811
|
+
},
|
|
1812
|
+
],
|
|
1813
|
+
chainId,
|
|
1814
|
+
feeToken: '0x20c0000000000000000000000000000000000001',
|
|
1815
|
+
keyAuthorization: keyAuth_signed,
|
|
1816
|
+
nonce: BigInt(nonce),
|
|
1817
|
+
gas: 1_000_000n,
|
|
1818
|
+
maxFeePerGas: Value.fromGwei('20'),
|
|
1819
|
+
maxPriorityFeePerGas: Value.fromGwei('10'),
|
|
1820
|
+
})
|
|
1821
|
+
|
|
1822
|
+
const signature = Secp256k1.sign({
|
|
1823
|
+
payload: TxEnvelopeTempo.getSignPayload(transaction, {
|
|
1824
|
+
from: root.address,
|
|
1825
|
+
}),
|
|
1826
|
+
privateKey: access.privateKey,
|
|
1827
|
+
})
|
|
1828
|
+
|
|
1829
|
+
const serialized_signed = TxEnvelopeTempo.serialize(transaction, {
|
|
1830
|
+
signature: SignatureEnvelope.from({
|
|
1831
|
+
userAddress: root.address,
|
|
1832
|
+
inner: SignatureEnvelope.from(signature),
|
|
1833
|
+
type: 'keychain',
|
|
1834
|
+
}),
|
|
1835
|
+
})
|
|
1836
|
+
|
|
1837
|
+
const receipt = (await client
|
|
1838
|
+
.request({
|
|
1839
|
+
method: 'eth_sendRawTransactionSync',
|
|
1840
|
+
params: [serialized_signed],
|
|
1841
|
+
})
|
|
1842
|
+
.then((tx) => TransactionReceipt.fromRpc(tx as any)))!
|
|
1843
|
+
expect(receipt).toBeDefined()
|
|
1844
|
+
expect(receipt.status).toBe('success')
|
|
1845
|
+
|
|
1846
|
+
{
|
|
1847
|
+
const response = await client
|
|
1848
|
+
.request({
|
|
1849
|
+
method: 'eth_getTransactionByHash',
|
|
1850
|
+
params: [receipt.transactionHash],
|
|
1851
|
+
})
|
|
1852
|
+
.then((tx) => Transaction.fromRpc(tx as any))
|
|
1853
|
+
if (!response) throw new Error()
|
|
1854
|
+
|
|
1855
|
+
expect(response.from).toBe(root.address)
|
|
1856
|
+
expect(response.keyAuthorization).toBeDefined()
|
|
1857
|
+
expect(response.keyAuthorization?.limits?.[0]?.limit).toBe(
|
|
1858
|
+
Value.from('1000', 6),
|
|
1859
|
+
)
|
|
1860
|
+
expect(response.keyAuthorization?.limits?.[0]?.period).toBe(2592000)
|
|
1861
|
+
}
|
|
1862
|
+
},
|
|
1863
|
+
)
|
|
1864
|
+
|
|
1865
|
+
// TODO: remove skipIf when testnet has T3
|
|
1866
|
+
test.skipIf(nodeEnv === 'testnet')(
|
|
1867
|
+
'behavior: rejects transfer exceeding periodic spending limit',
|
|
1868
|
+
async () => {
|
|
1869
|
+
const privateKey = Secp256k1.randomPrivateKey()
|
|
1870
|
+
const publicKey = Secp256k1.getPublicKey({ privateKey })
|
|
1871
|
+
const address = Address.fromPublicKey(publicKey)
|
|
1872
|
+
const token = '0x20c0000000000000000000000000000000000001'
|
|
1873
|
+
const transfer = AbiFunction.from(
|
|
1874
|
+
'function transfer(address to, uint256 amount)',
|
|
1875
|
+
)
|
|
1876
|
+
|
|
1877
|
+
// Key with a 5 USDC periodic limit
|
|
1878
|
+
const keyAuth = KeyAuthorization.from({
|
|
1879
|
+
address,
|
|
1880
|
+
chainId: BigInt(chainId),
|
|
1881
|
+
type: 'secp256k1',
|
|
1882
|
+
limits: [
|
|
1883
|
+
{
|
|
1884
|
+
token,
|
|
1885
|
+
limit: Value.from('5', 6),
|
|
1886
|
+
period: Period.months(1),
|
|
1887
|
+
},
|
|
1888
|
+
],
|
|
1889
|
+
})
|
|
1890
|
+
|
|
1891
|
+
const keyAuth_signed = KeyAuthorization.from(keyAuth, {
|
|
1892
|
+
signature: SignatureEnvelope.from(
|
|
1893
|
+
Secp256k1.sign({
|
|
1894
|
+
payload: KeyAuthorization.getSignPayload(keyAuth),
|
|
1895
|
+
privateKey: root.privateKey,
|
|
1896
|
+
}),
|
|
1897
|
+
),
|
|
1898
|
+
})
|
|
1899
|
+
|
|
1900
|
+
const nonce = await getTransactionCount(client, {
|
|
1901
|
+
address: root.address,
|
|
1902
|
+
blockTag: 'pending',
|
|
1903
|
+
})
|
|
1904
|
+
|
|
1905
|
+
// Try to transfer 10 USDC (exceeds 5 USDC limit)
|
|
1906
|
+
const transferData = AbiFunction.encodeData(transfer, [
|
|
1907
|
+
'0x0000000000000000000000000000000000000001',
|
|
1908
|
+
Value.from('10', 6),
|
|
1909
|
+
])
|
|
1910
|
+
|
|
1911
|
+
const transaction = TxEnvelopeTempo.from({
|
|
1912
|
+
calls: [{ to: token, data: transferData }],
|
|
1913
|
+
chainId,
|
|
1914
|
+
feeToken: token,
|
|
1915
|
+
keyAuthorization: keyAuth_signed,
|
|
1916
|
+
nonce: BigInt(nonce),
|
|
1917
|
+
gas: 5_000_000n,
|
|
1918
|
+
maxFeePerGas: Value.fromGwei('20'),
|
|
1919
|
+
maxPriorityFeePerGas: Value.fromGwei('10'),
|
|
1920
|
+
})
|
|
1921
|
+
|
|
1922
|
+
const signature = Secp256k1.sign({
|
|
1923
|
+
payload: TxEnvelopeTempo.getSignPayload(transaction, {
|
|
1924
|
+
from: root.address,
|
|
1925
|
+
}),
|
|
1926
|
+
privateKey,
|
|
1927
|
+
})
|
|
1928
|
+
|
|
1929
|
+
const serialized_signed = TxEnvelopeTempo.serialize(transaction, {
|
|
1930
|
+
signature: SignatureEnvelope.from({
|
|
1931
|
+
userAddress: root.address,
|
|
1932
|
+
inner: SignatureEnvelope.from(signature),
|
|
1933
|
+
type: 'keychain',
|
|
1934
|
+
}),
|
|
1935
|
+
})
|
|
1936
|
+
|
|
1937
|
+
const receipt = (await client
|
|
1938
|
+
.request({
|
|
1939
|
+
method: 'eth_sendRawTransactionSync',
|
|
1940
|
+
params: [serialized_signed],
|
|
1941
|
+
})
|
|
1942
|
+
.then((tx) => TransactionReceipt.fromRpc(tx as any)))!
|
|
1943
|
+
expect(receipt).toBeDefined()
|
|
1944
|
+
expect(receipt.status).toBe('reverted')
|
|
1945
|
+
},
|
|
1946
|
+
)
|
|
1947
|
+
|
|
1948
|
+
test.runIf(nodeEnv === 'localnet')(
|
|
1949
|
+
'behavior: periodic spending limit resets after period',
|
|
1950
|
+
async () => {
|
|
1951
|
+
const accessPrivateKey = Secp256k1.randomPrivateKey()
|
|
1952
|
+
const accessAddress = Address.fromPublicKey(
|
|
1953
|
+
Secp256k1.getPublicKey({ privateKey: accessPrivateKey }),
|
|
1954
|
+
)
|
|
1955
|
+
const token = '0x20c0000000000000000000000000000000000001'
|
|
1956
|
+
const transfer = AbiFunction.from(
|
|
1957
|
+
'function transfer(address to, uint256 amount)',
|
|
1958
|
+
)
|
|
1959
|
+
const recipient = '0x0000000000000000000000000000000000000001'
|
|
1960
|
+
|
|
1961
|
+
// Key with a 5 USDC limit that resets every 5 seconds
|
|
1962
|
+
const keyAuth = KeyAuthorization.from({
|
|
1963
|
+
address: accessAddress,
|
|
1964
|
+
chainId: BigInt(chainId),
|
|
1965
|
+
type: 'secp256k1',
|
|
1966
|
+
limits: [
|
|
1967
|
+
{
|
|
1968
|
+
token,
|
|
1969
|
+
limit: Value.from('5', 6),
|
|
1970
|
+
period: Period.seconds(5),
|
|
1971
|
+
},
|
|
1972
|
+
],
|
|
1973
|
+
})
|
|
1974
|
+
|
|
1975
|
+
const keyAuth_signed = KeyAuthorization.from(keyAuth, {
|
|
1976
|
+
signature: SignatureEnvelope.from(
|
|
1977
|
+
Secp256k1.sign({
|
|
1978
|
+
payload: KeyAuthorization.getSignPayload(keyAuth),
|
|
1979
|
+
privateKey: root.privateKey,
|
|
1980
|
+
}),
|
|
1981
|
+
),
|
|
1982
|
+
})
|
|
1983
|
+
|
|
1984
|
+
// 1. Provision key + transfer 4 USDC
|
|
1985
|
+
{
|
|
1986
|
+
const nonce = await getTransactionCount(client, {
|
|
1987
|
+
address: root.address,
|
|
1988
|
+
blockTag: 'pending',
|
|
1989
|
+
})
|
|
1990
|
+
|
|
1991
|
+
const transferData = AbiFunction.encodeData(transfer, [
|
|
1992
|
+
recipient,
|
|
1993
|
+
Value.from('4', 6),
|
|
1994
|
+
])
|
|
1995
|
+
|
|
1996
|
+
const transaction = TxEnvelopeTempo.from({
|
|
1997
|
+
calls: [{ to: token, data: transferData }],
|
|
1998
|
+
chainId,
|
|
1999
|
+
feeToken: token,
|
|
2000
|
+
keyAuthorization: keyAuth_signed,
|
|
2001
|
+
nonce: BigInt(nonce),
|
|
2002
|
+
gas: 5_000_000n,
|
|
2003
|
+
maxFeePerGas: Value.fromGwei('20'),
|
|
2004
|
+
maxPriorityFeePerGas: Value.fromGwei('10'),
|
|
2005
|
+
})
|
|
2006
|
+
|
|
2007
|
+
const signature = Secp256k1.sign({
|
|
2008
|
+
payload: TxEnvelopeTempo.getSignPayload(transaction, {
|
|
2009
|
+
from: root.address,
|
|
2010
|
+
}),
|
|
2011
|
+
privateKey: accessPrivateKey,
|
|
2012
|
+
})
|
|
2013
|
+
|
|
2014
|
+
const serialized = TxEnvelopeTempo.serialize(transaction, {
|
|
2015
|
+
signature: SignatureEnvelope.from({
|
|
2016
|
+
userAddress: root.address,
|
|
2017
|
+
inner: SignatureEnvelope.from(signature),
|
|
2018
|
+
type: 'keychain',
|
|
2019
|
+
}),
|
|
2020
|
+
})
|
|
2021
|
+
|
|
2022
|
+
const receipt = (await client
|
|
2023
|
+
.request({
|
|
2024
|
+
method: 'eth_sendRawTransactionSync',
|
|
2025
|
+
params: [serialized],
|
|
2026
|
+
})
|
|
2027
|
+
.then((tx) => TransactionReceipt.fromRpc(tx as any)))!
|
|
2028
|
+
expect(receipt.status).toBe('success')
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
// 2. Immediately try another 4 USDC transfer (should revert — limit exhausted)
|
|
2032
|
+
{
|
|
2033
|
+
const nonce = await getTransactionCount(client, {
|
|
2034
|
+
address: root.address,
|
|
2035
|
+
blockTag: 'pending',
|
|
2036
|
+
})
|
|
2037
|
+
|
|
2038
|
+
const transferData = AbiFunction.encodeData(transfer, [
|
|
2039
|
+
recipient,
|
|
2040
|
+
Value.from('4', 6),
|
|
2041
|
+
])
|
|
2042
|
+
|
|
2043
|
+
const transaction = TxEnvelopeTempo.from({
|
|
2044
|
+
calls: [{ to: token, data: transferData }],
|
|
2045
|
+
chainId,
|
|
2046
|
+
feeToken: token,
|
|
2047
|
+
nonce: BigInt(nonce),
|
|
2048
|
+
gas: 5_000_000n,
|
|
2049
|
+
maxFeePerGas: Value.fromGwei('20'),
|
|
2050
|
+
maxPriorityFeePerGas: Value.fromGwei('10'),
|
|
2051
|
+
})
|
|
2052
|
+
|
|
2053
|
+
const signature = Secp256k1.sign({
|
|
2054
|
+
payload: TxEnvelopeTempo.getSignPayload(transaction, {
|
|
2055
|
+
from: root.address,
|
|
2056
|
+
}),
|
|
2057
|
+
privateKey: accessPrivateKey,
|
|
2058
|
+
})
|
|
2059
|
+
|
|
2060
|
+
const serialized = TxEnvelopeTempo.serialize(transaction, {
|
|
2061
|
+
signature: SignatureEnvelope.from({
|
|
2062
|
+
userAddress: root.address,
|
|
2063
|
+
inner: SignatureEnvelope.from(signature),
|
|
2064
|
+
type: 'keychain',
|
|
2065
|
+
}),
|
|
2066
|
+
})
|
|
2067
|
+
|
|
2068
|
+
const receipt = (await client
|
|
2069
|
+
.request({
|
|
2070
|
+
method: 'eth_sendRawTransactionSync',
|
|
2071
|
+
params: [serialized],
|
|
2072
|
+
})
|
|
2073
|
+
.then((tx) => TransactionReceipt.fromRpc(tx as any)))!
|
|
2074
|
+
expect(receipt.status).toBe('reverted')
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
// 3. Wait for period to reset
|
|
2078
|
+
await new Promise((resolve) => setTimeout(resolve, 6000))
|
|
2079
|
+
|
|
2080
|
+
// 4. Transfer 4 USDC again (should succeed — period reset)
|
|
2081
|
+
{
|
|
2082
|
+
const nonce = await getTransactionCount(client, {
|
|
2083
|
+
address: root.address,
|
|
2084
|
+
blockTag: 'pending',
|
|
2085
|
+
})
|
|
2086
|
+
|
|
2087
|
+
const transferData = AbiFunction.encodeData(transfer, [
|
|
2088
|
+
recipient,
|
|
2089
|
+
Value.from('4', 6),
|
|
2090
|
+
])
|
|
2091
|
+
|
|
2092
|
+
const transaction = TxEnvelopeTempo.from({
|
|
2093
|
+
calls: [{ to: token, data: transferData }],
|
|
2094
|
+
chainId,
|
|
2095
|
+
feeToken: token,
|
|
2096
|
+
nonce: BigInt(nonce),
|
|
2097
|
+
gas: 5_000_000n,
|
|
2098
|
+
maxFeePerGas: Value.fromGwei('20'),
|
|
2099
|
+
maxPriorityFeePerGas: Value.fromGwei('10'),
|
|
2100
|
+
})
|
|
2101
|
+
|
|
2102
|
+
const signature = Secp256k1.sign({
|
|
2103
|
+
payload: TxEnvelopeTempo.getSignPayload(transaction, {
|
|
2104
|
+
from: root.address,
|
|
2105
|
+
}),
|
|
2106
|
+
privateKey: accessPrivateKey,
|
|
2107
|
+
})
|
|
2108
|
+
|
|
2109
|
+
const serialized = TxEnvelopeTempo.serialize(transaction, {
|
|
2110
|
+
signature: SignatureEnvelope.from({
|
|
2111
|
+
userAddress: root.address,
|
|
2112
|
+
inner: SignatureEnvelope.from(signature),
|
|
2113
|
+
type: 'keychain',
|
|
2114
|
+
}),
|
|
2115
|
+
})
|
|
2116
|
+
|
|
2117
|
+
const receipt = (await client
|
|
2118
|
+
.request({
|
|
2119
|
+
method: 'eth_sendRawTransactionSync',
|
|
2120
|
+
params: [serialized],
|
|
2121
|
+
})
|
|
2122
|
+
.then((tx) => TransactionReceipt.fromRpc(tx as any)))!
|
|
2123
|
+
expect(receipt.status).toBe('success')
|
|
2124
|
+
}
|
|
2125
|
+
},
|
|
2126
|
+
)
|
|
2127
|
+
|
|
2128
|
+
// TODO: remove skipIf when testnet has T3
|
|
2129
|
+
test.skipIf(nodeEnv === 'testnet')(
|
|
2130
|
+
'behavior: access key with call scopes (transfer)',
|
|
2131
|
+
async () => {
|
|
2132
|
+
const accessPrivateKey = Secp256k1.randomPrivateKey()
|
|
2133
|
+
const accessAddress = Address.fromPublicKey(
|
|
2134
|
+
Secp256k1.getPublicKey({ privateKey: accessPrivateKey }),
|
|
2135
|
+
)
|
|
2136
|
+
const recipient = '0x0000000000000000000000000000000000000001'
|
|
2137
|
+
const token = '0x20c0000000000000000000000000000000000001'
|
|
2138
|
+
const transfer = AbiFunction.from(
|
|
2139
|
+
'function transfer(address to, uint256 amount)',
|
|
2140
|
+
)
|
|
2141
|
+
const transferData = AbiFunction.encodeData(transfer, [
|
|
2142
|
+
recipient,
|
|
2143
|
+
Value.from('1', 6),
|
|
2144
|
+
])
|
|
2145
|
+
|
|
2146
|
+
// Scope key: only transfer() on token contract, with sufficient spending limit
|
|
2147
|
+
const keyAuth = KeyAuthorization.from({
|
|
2148
|
+
address: accessAddress,
|
|
2149
|
+
chainId: BigInt(chainId),
|
|
2150
|
+
type: 'secp256k1',
|
|
2151
|
+
limits: [{ token, limit: Value.from('10000', 6) }],
|
|
2152
|
+
scopes: [
|
|
2153
|
+
{
|
|
2154
|
+
address: token,
|
|
2155
|
+
selector: AbiFunction.getSelector(transfer),
|
|
2156
|
+
},
|
|
2157
|
+
],
|
|
2158
|
+
})
|
|
2159
|
+
|
|
2160
|
+
const keyAuth_signed = KeyAuthorization.from(keyAuth, {
|
|
2161
|
+
signature: SignatureEnvelope.from(
|
|
2162
|
+
Secp256k1.sign({
|
|
2163
|
+
payload: KeyAuthorization.getSignPayload(keyAuth),
|
|
2164
|
+
privateKey: root.privateKey,
|
|
2165
|
+
}),
|
|
2166
|
+
),
|
|
2167
|
+
})
|
|
2168
|
+
|
|
2169
|
+
const nonce = await getTransactionCount(client, {
|
|
2170
|
+
address: root.address,
|
|
2171
|
+
blockTag: 'pending',
|
|
2172
|
+
})
|
|
2173
|
+
|
|
2174
|
+
const transaction = TxEnvelopeTempo.from({
|
|
2175
|
+
calls: [{ to: token, data: transferData }],
|
|
2176
|
+
chainId,
|
|
2177
|
+
feeToken: token,
|
|
2178
|
+
keyAuthorization: keyAuth_signed,
|
|
2179
|
+
nonce: BigInt(nonce),
|
|
2180
|
+
gas: 5_000_000n,
|
|
2181
|
+
maxFeePerGas: Value.fromGwei('20'),
|
|
2182
|
+
maxPriorityFeePerGas: Value.fromGwei('10'),
|
|
2183
|
+
})
|
|
2184
|
+
|
|
2185
|
+
const signature = Secp256k1.sign({
|
|
2186
|
+
payload: TxEnvelopeTempo.getSignPayload(transaction, {
|
|
2187
|
+
from: root.address,
|
|
2188
|
+
}),
|
|
2189
|
+
privateKey: accessPrivateKey,
|
|
2190
|
+
})
|
|
2191
|
+
|
|
2192
|
+
const serialized_signed = TxEnvelopeTempo.serialize(transaction, {
|
|
2193
|
+
signature: SignatureEnvelope.from({
|
|
2194
|
+
userAddress: root.address,
|
|
2195
|
+
inner: SignatureEnvelope.from(signature),
|
|
2196
|
+
type: 'keychain',
|
|
2197
|
+
}),
|
|
2198
|
+
})
|
|
2199
|
+
|
|
2200
|
+
const receipt = (await client
|
|
2201
|
+
.request({
|
|
2202
|
+
method: 'eth_sendRawTransactionSync',
|
|
2203
|
+
params: [serialized_signed],
|
|
2204
|
+
})
|
|
2205
|
+
.then((tx) => TransactionReceipt.fromRpc(tx as any)))!
|
|
2206
|
+
expect(receipt).toBeDefined()
|
|
2207
|
+
expect(receipt.status).toBe('success')
|
|
2208
|
+
|
|
2209
|
+
{
|
|
2210
|
+
const response = await client
|
|
2211
|
+
.request({
|
|
2212
|
+
method: 'eth_getTransactionByHash',
|
|
2213
|
+
params: [receipt.transactionHash],
|
|
2214
|
+
})
|
|
2215
|
+
.then((tx) => Transaction.fromRpc(tx as any))
|
|
2216
|
+
if (!response) throw new Error()
|
|
2217
|
+
|
|
2218
|
+
expect(response.from).toBe(root.address)
|
|
2219
|
+
expect(response.keyAuthorization).toBeDefined()
|
|
2220
|
+
expect(response.keyAuthorization?.scopes?.[0]?.address).toBe(token)
|
|
2221
|
+
expect(response.keyAuthorization?.scopes?.[0]?.selector).toBe(
|
|
2222
|
+
'0xa9059cbb',
|
|
2223
|
+
)
|
|
2224
|
+
}
|
|
2225
|
+
},
|
|
2226
|
+
)
|
|
2227
|
+
|
|
2228
|
+
// TODO: remove skipIf when testnet has T3
|
|
2229
|
+
test.skipIf(nodeEnv === 'testnet')(
|
|
2230
|
+
'behavior: access key with call scopes + recipient allowlist (transfer)',
|
|
2231
|
+
async () => {
|
|
2232
|
+
const accessPrivateKey = Secp256k1.randomPrivateKey()
|
|
2233
|
+
const accessAddress = Address.fromPublicKey(
|
|
2234
|
+
Secp256k1.getPublicKey({ privateKey: accessPrivateKey }),
|
|
2235
|
+
)
|
|
2236
|
+
const recipient = '0x0000000000000000000000000000000000000001'
|
|
2237
|
+
const token = '0x20c0000000000000000000000000000000000001'
|
|
2238
|
+
const transfer = AbiFunction.from(
|
|
2239
|
+
'function transfer(address to, uint256 amount)',
|
|
2240
|
+
)
|
|
2241
|
+
const transferData = AbiFunction.encodeData(transfer, [
|
|
2242
|
+
recipient,
|
|
2243
|
+
Value.from('1', 6),
|
|
2244
|
+
])
|
|
2245
|
+
|
|
2246
|
+
// Scope key: transfer() on token, only to recipient, with sufficient spending limit
|
|
2247
|
+
const keyAuth = KeyAuthorization.from({
|
|
2248
|
+
address: accessAddress,
|
|
2249
|
+
chainId: BigInt(chainId),
|
|
2250
|
+
type: 'secp256k1',
|
|
2251
|
+
limits: [{ token, limit: Value.from('10000', 6) }],
|
|
2252
|
+
scopes: [
|
|
2253
|
+
{
|
|
2254
|
+
address: token,
|
|
2255
|
+
selector: AbiFunction.getSelector(transfer),
|
|
2256
|
+
recipients: [recipient],
|
|
2257
|
+
},
|
|
2258
|
+
],
|
|
2259
|
+
})
|
|
2260
|
+
|
|
2261
|
+
const keyAuth_signed = KeyAuthorization.from(keyAuth, {
|
|
2262
|
+
signature: SignatureEnvelope.from(
|
|
2263
|
+
Secp256k1.sign({
|
|
2264
|
+
payload: KeyAuthorization.getSignPayload(keyAuth),
|
|
2265
|
+
privateKey: root.privateKey,
|
|
2266
|
+
}),
|
|
2267
|
+
),
|
|
2268
|
+
})
|
|
2269
|
+
|
|
2270
|
+
const nonce = await getTransactionCount(client, {
|
|
2271
|
+
address: root.address,
|
|
2272
|
+
blockTag: 'pending',
|
|
2273
|
+
})
|
|
2274
|
+
|
|
2275
|
+
const transaction = TxEnvelopeTempo.from({
|
|
2276
|
+
calls: [{ to: token, data: transferData }],
|
|
2277
|
+
chainId,
|
|
2278
|
+
feeToken: token,
|
|
2279
|
+
keyAuthorization: keyAuth_signed,
|
|
2280
|
+
nonce: BigInt(nonce),
|
|
2281
|
+
gas: 5_000_000n,
|
|
2282
|
+
maxFeePerGas: Value.fromGwei('20'),
|
|
2283
|
+
maxPriorityFeePerGas: Value.fromGwei('10'),
|
|
2284
|
+
})
|
|
2285
|
+
|
|
2286
|
+
const signature = Secp256k1.sign({
|
|
2287
|
+
payload: TxEnvelopeTempo.getSignPayload(transaction, {
|
|
2288
|
+
from: root.address,
|
|
2289
|
+
}),
|
|
2290
|
+
privateKey: accessPrivateKey,
|
|
2291
|
+
})
|
|
2292
|
+
|
|
2293
|
+
const serialized_signed = TxEnvelopeTempo.serialize(transaction, {
|
|
2294
|
+
signature: SignatureEnvelope.from({
|
|
2295
|
+
userAddress: root.address,
|
|
2296
|
+
inner: SignatureEnvelope.from(signature),
|
|
2297
|
+
type: 'keychain',
|
|
2298
|
+
}),
|
|
2299
|
+
})
|
|
2300
|
+
|
|
2301
|
+
const receipt = (await client
|
|
2302
|
+
.request({
|
|
2303
|
+
method: 'eth_sendRawTransactionSync',
|
|
2304
|
+
params: [serialized_signed],
|
|
2305
|
+
})
|
|
2306
|
+
.then((tx) => TransactionReceipt.fromRpc(tx as any)))!
|
|
2307
|
+
expect(receipt).toBeDefined()
|
|
2308
|
+
expect(receipt.status).toBe('success')
|
|
2309
|
+
|
|
2310
|
+
{
|
|
2311
|
+
const response = await client
|
|
2312
|
+
.request({
|
|
2313
|
+
method: 'eth_getTransactionByHash',
|
|
2314
|
+
params: [receipt.transactionHash],
|
|
2315
|
+
})
|
|
2316
|
+
.then((tx) => Transaction.fromRpc(tx as any))
|
|
2317
|
+
if (!response) throw new Error()
|
|
2318
|
+
|
|
2319
|
+
expect(response.from).toBe(root.address)
|
|
2320
|
+
expect(response.keyAuthorization).toBeDefined()
|
|
2321
|
+
expect(response.keyAuthorization?.scopes?.[0]?.selector).toBe(
|
|
2322
|
+
'0xa9059cbb',
|
|
2323
|
+
)
|
|
2324
|
+
expect(response.keyAuthorization?.scopes?.[0]?.recipients).toEqual([
|
|
2325
|
+
recipient,
|
|
2326
|
+
])
|
|
2327
|
+
}
|
|
2328
|
+
},
|
|
2329
|
+
)
|
|
2330
|
+
|
|
2331
|
+
// TODO: remove skipIf when testnet has T3
|
|
2332
|
+
test.skipIf(nodeEnv === 'testnet')(
|
|
2333
|
+
'behavior: rejects transfer to wrong contract (outside scope)',
|
|
2334
|
+
async () => {
|
|
2335
|
+
const accessPrivateKey = Secp256k1.randomPrivateKey()
|
|
2336
|
+
const accessAddress = Address.fromPublicKey(
|
|
2337
|
+
Secp256k1.getPublicKey({ privateKey: accessPrivateKey }),
|
|
2338
|
+
)
|
|
2339
|
+
const token1 = '0x20c0000000000000000000000000000000000001'
|
|
2340
|
+
const token2 = '0x20c0000000000000000000000000000000000002'
|
|
2341
|
+
const transfer = AbiFunction.from(
|
|
2342
|
+
'function transfer(address to, uint256 amount)',
|
|
2343
|
+
)
|
|
2344
|
+
const transferData = AbiFunction.encodeData(transfer, [
|
|
2345
|
+
'0x0000000000000000000000000000000000000001',
|
|
2346
|
+
Value.from('1', 6),
|
|
2347
|
+
])
|
|
2348
|
+
|
|
2349
|
+
// Scope key to only token1
|
|
2350
|
+
const keyAuth = KeyAuthorization.from({
|
|
2351
|
+
address: accessAddress,
|
|
2352
|
+
chainId: BigInt(chainId),
|
|
2353
|
+
type: 'secp256k1',
|
|
2354
|
+
scopes: [
|
|
2355
|
+
{
|
|
2356
|
+
address: token1,
|
|
2357
|
+
selector: AbiFunction.getSelector(transfer),
|
|
2358
|
+
},
|
|
2359
|
+
],
|
|
2360
|
+
})
|
|
2361
|
+
|
|
2362
|
+
const keyAuth_signed = KeyAuthorization.from(keyAuth, {
|
|
2363
|
+
signature: SignatureEnvelope.from(
|
|
2364
|
+
Secp256k1.sign({
|
|
2365
|
+
payload: KeyAuthorization.getSignPayload(keyAuth),
|
|
2366
|
+
privateKey: root.privateKey,
|
|
2367
|
+
}),
|
|
2368
|
+
),
|
|
2369
|
+
})
|
|
2370
|
+
|
|
2371
|
+
const nonce = await getTransactionCount(client, {
|
|
2372
|
+
address: root.address,
|
|
2373
|
+
blockTag: 'pending',
|
|
2374
|
+
})
|
|
2375
|
+
|
|
2376
|
+
// Call transfer on token2 (not scoped) — should be rejected
|
|
2377
|
+
const transaction = TxEnvelopeTempo.from({
|
|
2378
|
+
calls: [{ to: token2, data: transferData }],
|
|
2379
|
+
chainId,
|
|
2380
|
+
feeToken: token1,
|
|
2381
|
+
keyAuthorization: keyAuth_signed,
|
|
2382
|
+
nonce: BigInt(nonce),
|
|
2383
|
+
gas: 5_000_000n,
|
|
2384
|
+
maxFeePerGas: Value.fromGwei('20'),
|
|
2385
|
+
maxPriorityFeePerGas: Value.fromGwei('10'),
|
|
2386
|
+
})
|
|
2387
|
+
|
|
2388
|
+
const signature = Secp256k1.sign({
|
|
2389
|
+
payload: TxEnvelopeTempo.getSignPayload(transaction, {
|
|
2390
|
+
from: root.address,
|
|
2391
|
+
}),
|
|
2392
|
+
privateKey: accessPrivateKey,
|
|
2393
|
+
})
|
|
2394
|
+
|
|
2395
|
+
const serialized_signed = TxEnvelopeTempo.serialize(transaction, {
|
|
2396
|
+
signature: SignatureEnvelope.from({
|
|
2397
|
+
userAddress: root.address,
|
|
2398
|
+
inner: SignatureEnvelope.from(signature),
|
|
2399
|
+
type: 'keychain',
|
|
2400
|
+
}),
|
|
2401
|
+
})
|
|
2402
|
+
|
|
2403
|
+
await expect(
|
|
2404
|
+
client.request({
|
|
2405
|
+
method: 'eth_sendRawTransactionSync',
|
|
2406
|
+
params: [serialized_signed],
|
|
2407
|
+
}),
|
|
2408
|
+
).rejects.toThrow()
|
|
2409
|
+
},
|
|
2410
|
+
)
|
|
2411
|
+
|
|
2412
|
+
// TODO: remove skipIf when testnet has T3
|
|
2413
|
+
test.skipIf(nodeEnv === 'testnet')(
|
|
2414
|
+
'behavior: rejects approve when only transfer is scoped',
|
|
2415
|
+
async () => {
|
|
2416
|
+
const accessPrivateKey = Secp256k1.randomPrivateKey()
|
|
2417
|
+
const accessAddress = Address.fromPublicKey(
|
|
2418
|
+
Secp256k1.getPublicKey({ privateKey: accessPrivateKey }),
|
|
2419
|
+
)
|
|
2420
|
+
const token = '0x20c0000000000000000000000000000000000001'
|
|
2421
|
+
const transfer = AbiFunction.from(
|
|
2422
|
+
'function transfer(address to, uint256 amount)',
|
|
2423
|
+
)
|
|
2424
|
+
const approve = AbiFunction.from(
|
|
2425
|
+
'function approve(address spender, uint256 amount)',
|
|
2426
|
+
)
|
|
2427
|
+
const approveData = AbiFunction.encodeData(approve, [
|
|
2428
|
+
'0x0000000000000000000000000000000000000001',
|
|
2429
|
+
Value.from('1', 6),
|
|
2430
|
+
])
|
|
2431
|
+
|
|
2432
|
+
// Scope key to only transfer()
|
|
2433
|
+
const keyAuth = KeyAuthorization.from({
|
|
2434
|
+
address: accessAddress,
|
|
2435
|
+
chainId: BigInt(chainId),
|
|
2436
|
+
type: 'secp256k1',
|
|
2437
|
+
scopes: [
|
|
2438
|
+
{
|
|
2439
|
+
address: token,
|
|
2440
|
+
selector: AbiFunction.getSelector(transfer),
|
|
2441
|
+
},
|
|
2442
|
+
],
|
|
2443
|
+
})
|
|
2444
|
+
|
|
2445
|
+
const keyAuth_signed = KeyAuthorization.from(keyAuth, {
|
|
2446
|
+
signature: SignatureEnvelope.from(
|
|
2447
|
+
Secp256k1.sign({
|
|
2448
|
+
payload: KeyAuthorization.getSignPayload(keyAuth),
|
|
2449
|
+
privateKey: root.privateKey,
|
|
2450
|
+
}),
|
|
2451
|
+
),
|
|
2452
|
+
})
|
|
2453
|
+
|
|
2454
|
+
const nonce = await getTransactionCount(client, {
|
|
2455
|
+
address: root.address,
|
|
2456
|
+
blockTag: 'pending',
|
|
2457
|
+
})
|
|
2458
|
+
|
|
2459
|
+
// Call approve() instead — should be rejected
|
|
2460
|
+
const transaction = TxEnvelopeTempo.from({
|
|
2461
|
+
calls: [{ to: token, data: approveData }],
|
|
2462
|
+
chainId,
|
|
2463
|
+
feeToken: token,
|
|
2464
|
+
keyAuthorization: keyAuth_signed,
|
|
2465
|
+
nonce: BigInt(nonce),
|
|
2466
|
+
gas: 5_000_000n,
|
|
2467
|
+
maxFeePerGas: Value.fromGwei('20'),
|
|
2468
|
+
maxPriorityFeePerGas: Value.fromGwei('10'),
|
|
2469
|
+
})
|
|
2470
|
+
|
|
2471
|
+
const signature = Secp256k1.sign({
|
|
2472
|
+
payload: TxEnvelopeTempo.getSignPayload(transaction, {
|
|
2473
|
+
from: root.address,
|
|
2474
|
+
}),
|
|
2475
|
+
privateKey: accessPrivateKey,
|
|
2476
|
+
})
|
|
2477
|
+
|
|
2478
|
+
const serialized_signed = TxEnvelopeTempo.serialize(transaction, {
|
|
2479
|
+
signature: SignatureEnvelope.from({
|
|
2480
|
+
userAddress: root.address,
|
|
2481
|
+
inner: SignatureEnvelope.from(signature),
|
|
2482
|
+
type: 'keychain',
|
|
2483
|
+
}),
|
|
2484
|
+
})
|
|
2485
|
+
|
|
2486
|
+
await expect(
|
|
2487
|
+
client.request({
|
|
2488
|
+
method: 'eth_sendRawTransactionSync',
|
|
2489
|
+
params: [serialized_signed],
|
|
2490
|
+
}),
|
|
2491
|
+
).rejects.toThrow()
|
|
2492
|
+
},
|
|
2493
|
+
)
|
|
2494
|
+
|
|
2495
|
+
// TODO: remove skipIf when testnet has T3
|
|
2496
|
+
test.skipIf(nodeEnv === 'testnet')(
|
|
2497
|
+
'behavior: rejects transfer to wrong recipient',
|
|
2498
|
+
async () => {
|
|
2499
|
+
const accessPrivateKey = Secp256k1.randomPrivateKey()
|
|
2500
|
+
const accessAddress = Address.fromPublicKey(
|
|
2501
|
+
Secp256k1.getPublicKey({ privateKey: accessPrivateKey }),
|
|
2502
|
+
)
|
|
2503
|
+
const token = '0x20c0000000000000000000000000000000000001'
|
|
2504
|
+
const allowedRecipient = '0x0000000000000000000000000000000000000001'
|
|
2505
|
+
const wrongRecipient = '0x0000000000000000000000000000000000000002'
|
|
2506
|
+
const transfer = AbiFunction.from(
|
|
2507
|
+
'function transfer(address to, uint256 amount)',
|
|
2508
|
+
)
|
|
2509
|
+
const transferData = AbiFunction.encodeData(transfer, [
|
|
2510
|
+
wrongRecipient,
|
|
2511
|
+
Value.from('1', 6),
|
|
2512
|
+
])
|
|
2513
|
+
|
|
2514
|
+
// Scope key: transfer only to allowedRecipient
|
|
2515
|
+
const keyAuth = KeyAuthorization.from({
|
|
2516
|
+
address: accessAddress,
|
|
2517
|
+
chainId: BigInt(chainId),
|
|
2518
|
+
type: 'secp256k1',
|
|
2519
|
+
scopes: [
|
|
2520
|
+
{
|
|
2521
|
+
address: token,
|
|
2522
|
+
selector: AbiFunction.getSelector(transfer),
|
|
2523
|
+
recipients: [allowedRecipient],
|
|
2524
|
+
},
|
|
2525
|
+
],
|
|
2526
|
+
})
|
|
2527
|
+
|
|
2528
|
+
const keyAuth_signed = KeyAuthorization.from(keyAuth, {
|
|
2529
|
+
signature: SignatureEnvelope.from(
|
|
2530
|
+
Secp256k1.sign({
|
|
2531
|
+
payload: KeyAuthorization.getSignPayload(keyAuth),
|
|
2532
|
+
privateKey: root.privateKey,
|
|
2533
|
+
}),
|
|
2534
|
+
),
|
|
2535
|
+
})
|
|
2536
|
+
|
|
2537
|
+
const nonce = await getTransactionCount(client, {
|
|
2538
|
+
address: root.address,
|
|
2539
|
+
blockTag: 'pending',
|
|
2540
|
+
})
|
|
2541
|
+
|
|
2542
|
+
// transfer to wrongRecipient — should be rejected
|
|
2543
|
+
const transaction = TxEnvelopeTempo.from({
|
|
2544
|
+
calls: [{ to: token, data: transferData }],
|
|
2545
|
+
chainId,
|
|
2546
|
+
feeToken: token,
|
|
2547
|
+
keyAuthorization: keyAuth_signed,
|
|
2548
|
+
nonce: BigInt(nonce),
|
|
2549
|
+
gas: 5_000_000n,
|
|
2550
|
+
maxFeePerGas: Value.fromGwei('20'),
|
|
2551
|
+
maxPriorityFeePerGas: Value.fromGwei('10'),
|
|
2552
|
+
})
|
|
2553
|
+
|
|
2554
|
+
const signature = Secp256k1.sign({
|
|
2555
|
+
payload: TxEnvelopeTempo.getSignPayload(transaction, {
|
|
2556
|
+
from: root.address,
|
|
2557
|
+
}),
|
|
2558
|
+
privateKey: accessPrivateKey,
|
|
2559
|
+
})
|
|
2560
|
+
|
|
2561
|
+
const serialized_signed = TxEnvelopeTempo.serialize(transaction, {
|
|
2562
|
+
signature: SignatureEnvelope.from({
|
|
2563
|
+
userAddress: root.address,
|
|
2564
|
+
inner: SignatureEnvelope.from(signature),
|
|
2565
|
+
type: 'keychain',
|
|
2566
|
+
}),
|
|
2567
|
+
})
|
|
2568
|
+
|
|
2569
|
+
await expect(
|
|
2570
|
+
client.request({
|
|
2571
|
+
method: 'eth_sendRawTransactionSync',
|
|
2572
|
+
params: [serialized_signed],
|
|
2573
|
+
}),
|
|
2574
|
+
).rejects.toThrow()
|
|
2575
|
+
},
|
|
2576
|
+
)
|
|
2577
|
+
|
|
2578
|
+
// TODO: remove skipIf when testnet has T3
|
|
2579
|
+
test.skipIf(nodeEnv === 'testnet')(
|
|
2580
|
+
'behavior: rejects any call when scopes = [] (empty)',
|
|
2581
|
+
async () => {
|
|
2582
|
+
const accessPrivateKey = Secp256k1.randomPrivateKey()
|
|
2583
|
+
const accessAddress = Address.fromPublicKey(
|
|
2584
|
+
Secp256k1.getPublicKey({ privateKey: accessPrivateKey }),
|
|
2585
|
+
)
|
|
2586
|
+
const token = '0x20c0000000000000000000000000000000000001'
|
|
2587
|
+
const transfer = AbiFunction.from(
|
|
2588
|
+
'function transfer(address to, uint256 amount)',
|
|
2589
|
+
)
|
|
2590
|
+
const transferData = AbiFunction.encodeData(transfer, [
|
|
2591
|
+
'0x0000000000000000000000000000000000000001',
|
|
2592
|
+
Value.from('1', 6),
|
|
2593
|
+
])
|
|
2594
|
+
|
|
2595
|
+
// scopes = [] → scoped mode with NO calls allowed
|
|
2596
|
+
const keyAuth = KeyAuthorization.from({
|
|
2597
|
+
address: accessAddress,
|
|
2598
|
+
chainId: BigInt(chainId),
|
|
2599
|
+
type: 'secp256k1',
|
|
2600
|
+
scopes: [],
|
|
2601
|
+
})
|
|
2602
|
+
|
|
2603
|
+
const keyAuth_signed = KeyAuthorization.from(keyAuth, {
|
|
2604
|
+
signature: SignatureEnvelope.from(
|
|
2605
|
+
Secp256k1.sign({
|
|
2606
|
+
payload: KeyAuthorization.getSignPayload(keyAuth),
|
|
2607
|
+
privateKey: root.privateKey,
|
|
2608
|
+
}),
|
|
2609
|
+
),
|
|
2610
|
+
})
|
|
2611
|
+
|
|
2612
|
+
const nonce = await getTransactionCount(client, {
|
|
2613
|
+
address: root.address,
|
|
2614
|
+
blockTag: 'pending',
|
|
2615
|
+
})
|
|
2616
|
+
|
|
2617
|
+
const transaction = TxEnvelopeTempo.from({
|
|
2618
|
+
calls: [{ to: token, data: transferData }],
|
|
2619
|
+
chainId,
|
|
2620
|
+
feeToken: token,
|
|
2621
|
+
keyAuthorization: keyAuth_signed,
|
|
2622
|
+
nonce: BigInt(nonce),
|
|
2623
|
+
gas: 5_000_000n,
|
|
2624
|
+
maxFeePerGas: Value.fromGwei('20'),
|
|
2625
|
+
maxPriorityFeePerGas: Value.fromGwei('10'),
|
|
2626
|
+
})
|
|
2627
|
+
|
|
2628
|
+
const signature = Secp256k1.sign({
|
|
2629
|
+
payload: TxEnvelopeTempo.getSignPayload(transaction, {
|
|
2630
|
+
from: root.address,
|
|
2631
|
+
}),
|
|
2632
|
+
privateKey: accessPrivateKey,
|
|
2633
|
+
})
|
|
2634
|
+
|
|
2635
|
+
const serialized_signed = TxEnvelopeTempo.serialize(transaction, {
|
|
2636
|
+
signature: SignatureEnvelope.from({
|
|
2637
|
+
userAddress: root.address,
|
|
2638
|
+
inner: SignatureEnvelope.from(signature),
|
|
2639
|
+
type: 'keychain',
|
|
2640
|
+
}),
|
|
2641
|
+
})
|
|
2642
|
+
|
|
2643
|
+
await expect(
|
|
2644
|
+
client.request({
|
|
2645
|
+
method: 'eth_sendRawTransactionSync',
|
|
2646
|
+
params: [serialized_signed],
|
|
2647
|
+
}),
|
|
2648
|
+
).rejects.toThrow()
|
|
2649
|
+
},
|
|
2650
|
+
)
|
|
1683
2651
|
})
|