dequanto 0.2.55 → 0.2.56
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/lib/cjs/abi/$abi.js +39 -1
- package/lib/cjs/abi/$abi.js.map +1 -1
- package/lib/cjs/contracts/ContractReader.js +2 -2
- package/lib/cjs/contracts/ContractReader.js.map +1 -1
- package/lib/cjs/safe/GnosisSafeHandler.js.map +1 -1
- package/lib/cjs/services/TimelockService/ITimelockService.js.map +1 -1
- package/lib/cjs/services/TimelockService/TimelockService.js +1 -1
- package/lib/cjs/services/TimelockService/TimelockService.js.map +1 -1
- package/lib/cjs/solidity/utils/$types.js +26 -0
- package/lib/cjs/solidity/utils/$types.js.map +1 -1
- package/lib/cjs/txs/TxWriter.js +6 -4
- package/lib/cjs/txs/TxWriter.js.map +1 -1
- package/lib/cjs/txs/agents/BatchAgent.js +32 -7
- package/lib/cjs/txs/agents/BatchAgent.js.map +1 -1
- package/lib/cjs/txs/agents/TxWriterAccountAgents.js +2 -0
- package/lib/cjs/txs/agents/TxWriterAccountAgents.js.map +1 -1
- package/lib/cjs/txs/agents/ZodiacAgent.js +20 -0
- package/lib/cjs/txs/agents/ZodiacAgent.js.map +1 -0
- package/lib/cjs/utils/$account.js +5 -1
- package/lib/cjs/utils/$account.js.map +1 -1
- package/lib/cjs/zodiac/ZodiacTx.js +83 -0
- package/lib/cjs/zodiac/ZodiacTx.js.map +1 -0
- package/lib/esm/abi/$abi.js.map +1 -1
- package/lib/esm/abi/$abi.mjs +36 -1
- package/lib/esm/contracts/ContractReader.js.map +1 -1
- package/lib/esm/contracts/ContractReader.mjs +2 -2
- package/lib/esm/safe/GnosisSafeHandler.js.map +1 -1
- package/lib/esm/services/TimelockService/ITimelockService.js.map +1 -1
- package/lib/esm/services/TimelockService/TimelockService.js.map +1 -1
- package/lib/esm/services/TimelockService/TimelockService.mjs +1 -1
- package/lib/esm/solidity/utils/$types.js.map +1 -1
- package/lib/esm/solidity/utils/$types.mjs +26 -0
- package/lib/esm/txs/TxWriter.js.map +1 -1
- package/lib/esm/txs/TxWriter.mjs +6 -4
- package/lib/esm/txs/agents/BatchAgent.js.map +1 -1
- package/lib/esm/txs/agents/BatchAgent.mjs +32 -7
- package/lib/esm/txs/agents/TxWriterAccountAgents.js.map +1 -1
- package/lib/esm/txs/agents/TxWriterAccountAgents.mjs +2 -0
- package/lib/esm/txs/agents/ZodiacAgent.js.map +1 -0
- package/lib/esm/txs/agents/ZodiacAgent.mjs +16 -0
- package/lib/esm/utils/$account.js.map +1 -1
- package/lib/esm/utils/$account.mjs +5 -1
- package/lib/esm/zodiac/ZodiacTx.js.map +1 -0
- package/lib/esm/zodiac/ZodiacTx.mjs +79 -0
- package/lib/types/contracts/ContractReader.d.ts +1 -1
- package/lib/types/models/TAccount.d.ts +11 -2
- package/lib/types/models/TEth.d.ts +4 -1
- package/lib/types/services/TimelockService/ITimelockService.d.ts +1 -0
- package/lib/types/solidity/utils/$types.d.ts +8 -0
- package/lib/types/txs/agents/BatchAgent.d.ts +2 -1
- package/lib/types/txs/agents/ZodiacAgent.d.ts +7 -0
- package/lib/types/utils/$account.d.ts +2 -1
- package/lib/types/zodiac/ZodiacTx.d.ts +30 -0
- package/package.json +1 -1
- package/skills/dequanto/INDEX.md +1 -1
- package/skills/dequanto/SKILL.md +4 -3
- package/skills/dequanto/examples/browser-wallet.spec.ts +2 -1
- package/skills/dequanto/examples/config.spec.ts +2 -1
- package/skills/dequanto/examples/contract-read.spec.ts +2 -1
- package/skills/dequanto/examples/contract-write.spec.ts +2 -1
- package/skills/dequanto/examples/deploy-contract.spec.ts +2 -1
- package/skills/dequanto/examples/erc4337-userop.spec.ts +2 -1
- package/skills/dequanto/examples/explorer-abi.spec.ts +2 -1
- package/skills/dequanto/examples/generate-contract-class.spec.ts +2 -1
- package/skills/dequanto/examples/hardhat-debug.spec.ts +2 -1
- package/skills/dequanto/examples/hardhat-fork.spec.ts +2 -1
- package/skills/dequanto/examples/index-events.spec.ts +2 -1
- package/skills/dequanto/examples/live-events.spec.ts +2 -1
- package/skills/dequanto/examples/revert-error.spec.ts +2 -1
- package/skills/dequanto/examples/rpc-read.spec.ts +19 -5
- package/skills/dequanto/examples/safe-batch.spec.ts +92 -3
- package/skills/dequanto/examples/send-transaction.spec.ts +2 -1
- package/skills/dequanto/examples/storage.spec.ts +2 -1
- package/skills/dequanto/examples/token-transfer.spec.ts +2 -1
- package/skills/dequanto/examples/utils.spec.ts +2 -1
- package/skills/dequanto/references/safe-erc4337.md +50 -3
- package/skills/dequanto/references/tokens-accounts.md +1 -0
- package/skills/dequanto/references/transactions.md +2 -1
- package/src/abi/$abi.ts +36 -0
- package/src/contracts/ContractReader.ts +2 -2
- package/src/models/TAccount.ts +17 -2
- package/src/models/TEth.ts +4 -1
- package/src/safe/GnosisSafeHandler.ts +12 -12
- package/src/services/TimelockService/ITimelockService.ts +2 -0
- package/src/services/TimelockService/TimelockService.ts +1 -1
- package/src/solidity/utils/$types.ts +27 -0
- package/src/txs/TxWriter.ts +6 -4
- package/src/txs/agents/BatchAgent.ts +33 -7
- package/src/txs/agents/TxWriterAccountAgents.ts +2 -0
- package/src/txs/agents/ZodiacAgent.ts +19 -0
- package/src/utils/$account.ts +6 -2
- package/src/zodiac/ZodiacTx.ts +99 -0
- package/typings/index.d.ts +1 -4
- package/typings/globals/atma-utest/index.d.ts +0 -53
- package/typings/globals/atma-utest/typings.json +0 -10
- package/typings/globals/axios/index.d.ts +0 -6
- package/typings/globals/mask/index.d.ts +0 -6
- package/typings/globals/mask/typings.json +0 -9
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { UTest } from 'atma-utest';
|
|
1
2
|
import { Web3ClientFactory } from 'dequanto/clients/Web3ClientFactory';
|
|
2
3
|
import { ERC20 } from 'dequanto/prebuilt/openzeppelin/ERC20';
|
|
3
4
|
import { $erc20 } from 'dequanto/utils/$erc20';
|
|
4
5
|
import { $require } from 'dequanto/utils/$require';
|
|
5
6
|
|
|
6
|
-
UTest({
|
|
7
|
+
UTest.create({
|
|
7
8
|
async 'set an ERC20 balance and transfer from an impersonated account on a fork' () {
|
|
8
9
|
// Requires an accessible Ethereum RPC from the dequanto configuration.
|
|
9
10
|
const client = await Web3ClientFactory.getAsync('hh:memory:eth');
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { UTest } from 'atma-utest';
|
|
1
2
|
import { Web3ClientFactory } from 'dequanto/clients/Web3ClientFactory';
|
|
2
3
|
import { EventsIndexer } from 'dequanto/indexer/EventsIndexer';
|
|
3
4
|
import { ERC20 } from 'dequanto/prebuilt/openzeppelin/ERC20';
|
|
4
5
|
import { $require } from 'dequanto/utils/$require';
|
|
5
6
|
|
|
6
|
-
UTest({
|
|
7
|
+
UTest.create({
|
|
7
8
|
async 'index stable USDC Transfer logs from live Ethereum RPC' () {
|
|
8
9
|
const client = await Web3ClientFactory.getAsync('eth');
|
|
9
10
|
const usdc = new ERC20('0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', client);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { UTest } from 'atma-utest';
|
|
1
2
|
import { TEth } from 'dequanto/models/TEth';
|
|
2
3
|
import { Web3ClientFactory } from 'dequanto/clients/Web3ClientFactory';
|
|
3
4
|
import { ERC20 } from 'dequanto/prebuilt/openzeppelin/ERC20';
|
|
@@ -9,7 +10,7 @@ import { $require } from 'dequanto/utils/$require';
|
|
|
9
10
|
const USDC = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48' as const;
|
|
10
11
|
const TRANSFER_AMOUNT = 1_000_000n; // 1 USDC (6 decimals).
|
|
11
12
|
|
|
12
|
-
UTest({
|
|
13
|
+
UTest.create({
|
|
13
14
|
$config: {
|
|
14
15
|
timeout: 60_000
|
|
15
16
|
},
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { UTest } from 'atma-utest';
|
|
1
2
|
import { HardhatProvider } from 'dequanto/hardhat/HardhatProvider';
|
|
2
3
|
import { $account } from 'dequanto/utils/$account';
|
|
3
4
|
import { $promise } from 'dequanto/utils/$promise';
|
|
@@ -8,7 +9,7 @@ import { $require } from 'dequanto/utils/$require';
|
|
|
8
9
|
// if it reverts, dequanto catches the revert, decodes any custom error bytecode using the ABI,
|
|
9
10
|
// captures execution traces, and throws a standard JavaScript Error.
|
|
10
11
|
// If gas estimation succeeds but the transaction reverts on-chain later, the error flow is identical.
|
|
11
|
-
UTest({
|
|
12
|
+
UTest.create({
|
|
12
13
|
async 'catch automatic revert and decode custom error without manual simulation' () {
|
|
13
14
|
const hh = new HardhatProvider();
|
|
14
15
|
const client = hh.client();
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { UTest } from 'atma-utest';
|
|
1
2
|
import { Web3ClientFactory } from 'dequanto/clients/Web3ClientFactory';
|
|
2
3
|
import { BlockDateResolver } from 'dequanto/blocks/BlockDateResolver';
|
|
3
4
|
import { Config } from 'dequanto/config/Config';
|
|
4
5
|
import { $date } from 'dequanto/utils/$date';
|
|
5
6
|
import { $require } from 'dequanto/utils/$require';
|
|
6
7
|
|
|
7
|
-
UTest({
|
|
8
|
+
UTest.create({
|
|
8
9
|
async 'read common RPC data via Web3Client directly' () {
|
|
9
10
|
const cfg = await Config.fetch();
|
|
10
11
|
|
|
@@ -51,9 +52,22 @@ UTest({
|
|
|
51
52
|
const info = await resolver.getBlockInfoFor(date);
|
|
52
53
|
const blockDate = await resolver.getDate(blockNumber);
|
|
53
54
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
/* max ~1 block diff */
|
|
56
|
+
const block = {
|
|
57
|
+
24576294: {
|
|
58
|
+
nr: 24576294,
|
|
59
|
+
date: new Date('2026-03-03T10:19:47Z'),
|
|
60
|
+
},
|
|
61
|
+
24576295: {
|
|
62
|
+
nr: 24576295,
|
|
63
|
+
date: new Date('2026-03-03T10:19:59Z'),
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
const blockData = block[info.block];
|
|
67
|
+
$require.notNull(blockData, `No info for the fetched block ${info.block}`);
|
|
68
|
+
$require.eq(blockNumber, info.block);
|
|
69
|
+
$require.eq(blockDate.toISOString(), blockData.date.toISOString());
|
|
70
|
+
$require.lte(Math.abs($date.toUnixTimestamp(blockData.date) - info.timestamp), 1);
|
|
71
|
+
|
|
58
72
|
}
|
|
59
73
|
});
|
|
@@ -1,12 +1,63 @@
|
|
|
1
|
+
import { UTest } from 'atma-utest';
|
|
2
|
+
import { Config } from 'dequanto/config/Config';
|
|
3
|
+
import { GnosisSafeFactory } from 'dequanto/safe/GnosisSafeFactory';
|
|
4
|
+
import { BatchAgent } from 'dequanto/txs/agents/BatchAgent';
|
|
5
|
+
import { TxWriter } from 'dequanto/txs/TxWriter';
|
|
1
6
|
import { HardhatProvider } from 'dequanto/hardhat/HardhatProvider';
|
|
2
7
|
import { InMemoryServiceTransport } from 'dequanto/safe/transport/InMemoryServiceTransport';
|
|
3
8
|
import { SafeTx } from 'dequanto/safe/SafeTx';
|
|
4
9
|
import { $address } from 'dequanto/utils/$address';
|
|
5
10
|
import { $require } from 'dequanto/utils/$require';
|
|
6
11
|
import { SafeAccount } from 'dequanto/models/TAccount';
|
|
12
|
+
import { Web3Client } from 'dequanto/clients/Web3Client';
|
|
13
|
+
import { TEth } from 'dequanto/models/TEth';
|
|
7
14
|
|
|
8
|
-
UTest({
|
|
9
|
-
async '
|
|
15
|
+
UTest.create({
|
|
16
|
+
async 'collect normal contract calls and execute one Safe batch'() {
|
|
17
|
+
let provider = new HardhatProvider();
|
|
18
|
+
let client = provider.client();
|
|
19
|
+
let owner = provider.deployer(0);
|
|
20
|
+
let receiver = provider.deployer(1);
|
|
21
|
+
|
|
22
|
+
// Local setup; on a live network, use an existing Safe and configured MultiSend.
|
|
23
|
+
let { safeAddress } = await deployLocalTestSafe(owner, provider, client);
|
|
24
|
+
let { token } = await deployLocalTestToken(provider, client);
|
|
25
|
+
let safe: SafeAccount = {
|
|
26
|
+
type: 'safe',
|
|
27
|
+
platform: client.platform,
|
|
28
|
+
address: safeAddress,
|
|
29
|
+
operator: owner,
|
|
30
|
+
owners: [owner]
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Dequanto signs and executes when available signers meet the Safe threshold.
|
|
34
|
+
// Otherwise, it submits to the Safe Service API and waits for confirmations.
|
|
35
|
+
TxWriter.DEFAULTS.safeTransport = new InMemoryServiceTransport(client, owner);
|
|
36
|
+
|
|
37
|
+
let batch = new BatchAgent().enable();
|
|
38
|
+
try {
|
|
39
|
+
// Normal write calls return mock receipts and queue transactions without submitting them.
|
|
40
|
+
await token.$receipt().transfer(safe, receiver.address, 50n);
|
|
41
|
+
await token.$receipt().transfer(safe, receiver.address, 25n);
|
|
42
|
+
|
|
43
|
+
$require.eq(batch.transactions.length, 2);
|
|
44
|
+
$require.eq(await token.received(receiver.address), 0n);
|
|
45
|
+
|
|
46
|
+
// Detects the Safe sender and submits both calls as one Safe batch transaction.
|
|
47
|
+
let writers = await batch.execute();
|
|
48
|
+
|
|
49
|
+
$require.eq(writers.length, 1);
|
|
50
|
+
$require.eq(await token.received(receiver.address), 75n);
|
|
51
|
+
$require.True($address.eq(await token.lastSender(), safe.address));
|
|
52
|
+
|
|
53
|
+
} finally {
|
|
54
|
+
batch.disable();
|
|
55
|
+
TxWriter.DEFAULTS.agent = null;
|
|
56
|
+
TxWriter.DEFAULTS.safeTransport = null;
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
async 'build a Safe batch from generated tx data'() {
|
|
10
61
|
const provider = new HardhatProvider();
|
|
11
62
|
const client = provider.client();
|
|
12
63
|
const owner1 = provider.deployer(0);
|
|
@@ -53,4 +104,42 @@ UTest({
|
|
|
53
104
|
});
|
|
54
105
|
$require.notNull(safeTx);
|
|
55
106
|
}
|
|
56
|
-
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
async function deployLocalTestSafe(owner: TEth.EoAccount, provider: HardhatProvider, client: Web3Client) {
|
|
111
|
+
let config = await Config.get();
|
|
112
|
+
let { contract: singleton } = await provider.deploySol('/test/fixtures/gnosis/GnosisSafe.sol', { client });
|
|
113
|
+
let { contract: proxyFactory } = await provider.deploySol('/test/fixtures/gnosis/proxies/GnosisSafeProxyFactory.sol', { client });
|
|
114
|
+
let { contract: multiSend } = await provider.deploySol('/test/fixtures/gnosis/libraries/MultiSend.sol', { client });
|
|
115
|
+
|
|
116
|
+
config.safe.contracts[client.platform] = {
|
|
117
|
+
Safe: singleton.address,
|
|
118
|
+
SafeProxyFactory: proxyFactory.address,
|
|
119
|
+
MultiSend: multiSend.address
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
let handler = await GnosisSafeFactory.create(owner, client, {
|
|
123
|
+
owners: [owner.address]
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
safeAddress: handler.safeAddress
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function deployLocalTestToken(provider: HardhatProvider, client: Web3Client) {
|
|
132
|
+
let { contract: token } = await provider.deployCode(`
|
|
133
|
+
contract Token {
|
|
134
|
+
mapping(address => uint256) public received;
|
|
135
|
+
address public lastSender;
|
|
136
|
+
function transfer(address to, uint256 amount) external returns (bool) {
|
|
137
|
+
received[to] += amount;
|
|
138
|
+
lastSender = msg.sender;
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
`, { client });
|
|
143
|
+
|
|
144
|
+
return { token };
|
|
145
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { UTest } from 'atma-utest';
|
|
1
2
|
import { Web3ClientFactory } from 'dequanto/clients/Web3ClientFactory';
|
|
2
3
|
import { TxDataBuilder } from 'dequanto/txs/TxDataBuilder';
|
|
3
4
|
import { TxWriter } from 'dequanto/txs/TxWriter';
|
|
@@ -6,7 +7,7 @@ import { $address } from 'dequanto/utils/$address';
|
|
|
6
7
|
import { $number } from 'dequanto/utils/$number';
|
|
7
8
|
import { $require } from 'dequanto/utils/$require';
|
|
8
9
|
|
|
9
|
-
UTest({
|
|
10
|
+
UTest.create({
|
|
10
11
|
async 'build native transfer tx data with gas fields' () {
|
|
11
12
|
const client = await Web3ClientFactory.getAsync('hh:memory');
|
|
12
13
|
const alice = $account.generate('alice');
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { UTest } from 'atma-utest';
|
|
1
2
|
import { AavePool } from '../../../0xc/eth/AavePool/AavePool';
|
|
2
3
|
import { Web3ClientFactory } from 'dequanto/clients/Web3ClientFactory';
|
|
3
4
|
import { HardhatProvider } from 'dequanto/hardhat/HardhatProvider';
|
|
@@ -7,7 +8,7 @@ import { $require } from 'dequanto/utils/$require';
|
|
|
7
8
|
|
|
8
9
|
// Generated classes include the storage layout: use instance.storage.$set(name, value).
|
|
9
10
|
// SlotsParser and SlotsStorage provide the low-level alternative for Solidity source.
|
|
10
|
-
UTest({
|
|
11
|
+
UTest.create({
|
|
11
12
|
async 'read and set AavePool storage by variable name on an Ethereum fork' () {
|
|
12
13
|
const client = await Web3ClientFactory.getAsync('hh:memory:eth');
|
|
13
14
|
const pool = new AavePool(undefined, client);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { UTest } from 'atma-utest';
|
|
1
2
|
import { Web3ClientFactory } from 'dequanto/clients/Web3ClientFactory';
|
|
2
3
|
import { ERC20 } from 'dequanto/prebuilt/openzeppelin/ERC20';
|
|
3
4
|
import { $account } from 'dequanto/utils/$account';
|
|
@@ -8,7 +9,7 @@ const USDC = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48' as const;
|
|
|
8
9
|
const INITIAL_SENDER_BALANCE = 100_000_000n;
|
|
9
10
|
const TRANSFER_AMOUNT = 42_000_000n;
|
|
10
11
|
|
|
11
|
-
UTest({
|
|
12
|
+
UTest.create({
|
|
12
13
|
async 'transfer USDC with a prebuilt ERC20 contract class' () {
|
|
13
14
|
const client = await Web3ClientFactory.getAsync('hh:memory:eth');
|
|
14
15
|
const sender = $account.generate('sender');
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { UTest } from 'atma-utest';
|
|
1
2
|
import { $abi } from 'dequanto/abi/$abi';
|
|
2
3
|
import { $contract } from 'dequanto/utils/$contract';
|
|
3
4
|
import { $date } from 'dequanto/utils/$date';
|
|
@@ -8,7 +9,7 @@ import { $sig } from 'dequanto/utils/$sig';
|
|
|
8
9
|
|
|
9
10
|
const ACCOUNT = '0x0000000000000000000000000000000000000002';
|
|
10
11
|
|
|
11
|
-
UTest({
|
|
12
|
+
UTest.create({
|
|
12
13
|
'encode and decode ABI values and calldata' () {
|
|
13
14
|
const encoded = $abi.encode(['address', 'uint256'], [ACCOUNT, 5n]);
|
|
14
15
|
const [account, amount] = $abi.decode(['address', 'uint256'], encoded);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# Safe And ERC-4337
|
|
1
|
+
# Safe, Zodiac Roles, And ERC-4337
|
|
2
2
|
|
|
3
|
-
Use dequanto account-agent flows for Gnosis Safe and ERC-4337 instead of wiring external SDK calls directly.
|
|
3
|
+
Use dequanto account-agent flows for Gnosis Safe, Zodiac Roles, and ERC-4337 instead of wiring external SDK calls directly.
|
|
4
4
|
|
|
5
5
|
Key source files:
|
|
6
6
|
|
|
@@ -8,6 +8,8 @@ Key source files:
|
|
|
8
8
|
- `src/safe/GnosisSafeFactory.ts`
|
|
9
9
|
- `src/safe/GnosisSafeService.ts`
|
|
10
10
|
- `src/safe/SafeTx.ts`
|
|
11
|
+
- `src/zodiac/ZodiacTx.ts`
|
|
12
|
+
- `src/txs/agents/ZodiacAgent.ts`
|
|
11
13
|
- `src/safe/transport/InMemoryServiceTransport.ts`
|
|
12
14
|
- `src/safe/transport/FileServiceTransport.ts`
|
|
13
15
|
- `src/safe/transport/SafeServiceTransport.ts`
|
|
@@ -20,6 +22,8 @@ Key source files:
|
|
|
20
22
|
Useful tests:
|
|
21
23
|
|
|
22
24
|
- `test/safe/safe.spec.ts`
|
|
25
|
+
- `test/zodiac/ZodiacTx.spec.ts`
|
|
26
|
+
- `test/zodiac/ZodiacAgent.spec.ts`
|
|
23
27
|
- `test/erc4337/erc4337.spec.ts`
|
|
24
28
|
|
|
25
29
|
## Safe Account Transaction
|
|
@@ -101,6 +105,49 @@ See `./transactions.md` for the full `BatchAgent` script pattern.
|
|
|
101
105
|
|
|
102
106
|
Use `GnosisSafeService.decodeSafeTx(data, { decodeContractCall: true })`. Register ABIs on the explorer when decoding local or unknown contracts.
|
|
103
107
|
|
|
108
|
+
## Zodiac Roles Account Transaction
|
|
109
|
+
|
|
110
|
+
Use `ZodiacAccount` for a Zodiac Roles Modifier with the bytes32 role-key API:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
import { ZodiacAccount } from 'dequanto/models/TAccount';
|
|
114
|
+
|
|
115
|
+
let zodiacAccount: ZodiacAccount = {
|
|
116
|
+
type: 'zodiac',
|
|
117
|
+
platform: client.platform,
|
|
118
|
+
address: rolesModifierAddress,
|
|
119
|
+
operator,
|
|
120
|
+
roleKey,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
let tx = await token.$receipt().transfer(zodiacAccount, receiver, amount);
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`address` is the Roles Modifier address; `operator` is the EOA authorized for `roleKey`. Supply the role key as hex, not a role name; `ZodiacTx` pads it to 32 bytes. `ZodiacAgent` wraps the original call in `execTransactionWithRole(..., shouldRevert: true)` and the operator submits it directly on-chain. The Safe/avatar pays the inner call's native value; the operator pays gas.
|
|
127
|
+
|
|
128
|
+
## Zodiac Batch
|
|
129
|
+
|
|
130
|
+
Use `ZodiacTx` to compose a batch from generated `$data()` calls:
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
import { ZodiacTx } from 'dequanto/zodiac/ZodiacTx';
|
|
134
|
+
|
|
135
|
+
let zodiac = new ZodiacTx(zodiacAccount, client);
|
|
136
|
+
let tx = await zodiac.executeBatch(
|
|
137
|
+
await token.$data().approve(zodiacAccount, spender, amount),
|
|
138
|
+
await token.$data().transfer(zodiacAccount, receiver, amount)
|
|
139
|
+
);
|
|
140
|
+
await tx.wait();
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Each inner transaction uses CALL. The Roles transaction uses DELEGATECALL to MultiSend. The helper resolves the deployment from `config.safe.contracts[client.network]`, preferring `MultiSendCallOnly` and falling back to `MultiSend`, just like `SafeTx`. Use `options.contracts` for deployment overrides; there is no MultiSend address field on the account.
|
|
144
|
+
|
|
145
|
+
The Roles Modifier must have a transaction unwrapper registered for the selected MultiSend deployment to check each inner call's permissions.
|
|
146
|
+
|
|
147
|
+
Use `execute(call)` for a single transaction. `getTxData(call)` and `getBatchTxData(...calls)` return the encoded transaction to the Roles Modifier without submitting it. These results are already wrapped; submit them as the operator, not as a Zodiac account.
|
|
148
|
+
|
|
149
|
+
`BatchAgent` also collects ordinary contract writes with a Zodiac account and submits them through `ZodiacTx`. It keeps different roles, operators, and chains in separate groups. See `./transactions.md` for the script pattern.
|
|
150
|
+
|
|
104
151
|
## ERC-4337
|
|
105
152
|
|
|
106
153
|
Use `Erc4337TxWriter` for the higher-level flow:
|
|
@@ -130,5 +177,5 @@ Use `Erc4337Service` for low-level operation creation, signing, decoding, and `h
|
|
|
130
177
|
## Avoid
|
|
131
178
|
|
|
132
179
|
- Do not manually assemble Safe `execTransaction` signatures unless the task is explicitly low-level.
|
|
133
|
-
- Do not bypass dequanto `TxWriter` account agents for Safe or ERC-4337 when generated contract writes can use account types.
|
|
180
|
+
- Do not bypass dequanto `TxWriter` account agents for Safe, Zodiac, or ERC-4337 when generated contract writes can use account types.
|
|
134
181
|
|
|
@@ -93,6 +93,7 @@ Transaction APIs accept several account shapes:
|
|
|
93
93
|
- named account: string resolved through `ChainAccountService`
|
|
94
94
|
- impersonated account in Hardhat: `{ type: 'impersonated', address }` (no private key required)
|
|
95
95
|
- Safe account: `{ type: 'safe', address, operator, owners? }`
|
|
96
|
+
- Zodiac Roles account: `{ type: 'zodiac', platform, address: rolesModifierAddress, operator, roleKey }`; the operator is an EOA and the role key is hex. See [Zodiac Roles](safe-erc4337.md#zodiac-roles-account-transaction) for execution and batching.
|
|
96
97
|
- ERC-4337 account: `{ type: 'erc4337', address, operator }`
|
|
97
98
|
|
|
98
99
|
Inspect `src/models/TAccount.ts` before adding a new account shape.
|
|
@@ -72,6 +72,7 @@ Important behavior:
|
|
|
72
72
|
- `batch.execute()` skips duplicate transactions by default; pass `{ includeDuplicates: true }` when duplicates are intentional.
|
|
73
73
|
- Deployment transactions are ignored by default; pass `new BatchAgent({ ignoreContractCreation: false })` when contract creation should also be intercepted.
|
|
74
74
|
- If the account being submitted is a Safe, `execute()` creates one Safe multicall/batch transaction from many cached single transactions.
|
|
75
|
+
- If the account being submitted is Zodiac, `execute()` wraps each compatible group in MultiSend and submits it through `execTransactionWithRole`. Different roles, operators, and chains remain separate. See [Zodiac batching](safe-erc4337.md#zodiac-batch) for deployment configuration and the required unwrapper.
|
|
75
76
|
- If the account being submitted is a Timelock, `execute()` creates one Timelock batch/scheduleBatch flow from many cached single transactions.
|
|
76
77
|
- For normal EOA accounts, `execute()` sends each cached transaction on-chain.
|
|
77
78
|
|
|
@@ -165,5 +166,5 @@ See the [revert and custom error example](../examples/revert-error.spec.ts) for
|
|
|
165
166
|
|
|
166
167
|
- Do not perform manual simulations with `$call()` or `$gas()` solely to catch reverts before sending; `TxWriter` automatically simulates via gas estimation, decodes custom errors, and throws standard JavaScript errors.
|
|
167
168
|
- Do not manually manage gas and nonce unless the task needs it.
|
|
168
|
-
- Do not use external wallet clients when a dequanto `TAccount`, `SafeAccount`, or `Erc4337Account` flow is available.
|
|
169
|
+
- Do not use external wallet clients when a dequanto `TAccount`, `SafeAccount`, `ZodiacAccount`, or `Erc4337Account` flow is available.
|
|
169
170
|
|
package/src/abi/$abi.ts
CHANGED
|
@@ -4,6 +4,9 @@ import { TEth } from 'dequanto/models/TEth';
|
|
|
4
4
|
import { $abiCoder } from './$abiCoder';
|
|
5
5
|
import { $abiUtils } from 'dequanto/utils/$abiUtils';
|
|
6
6
|
import { $abiParser } from 'dequanto/utils/$abiParser';
|
|
7
|
+
import { $hex } from 'dequanto/utils/$hex';
|
|
8
|
+
import alot from 'alot';
|
|
9
|
+
import { $types } from 'dequanto/solidity/utils/$types';
|
|
7
10
|
|
|
8
11
|
export namespace $abi {
|
|
9
12
|
type TAbiType = string | ParamType | TAbiInput;
|
|
@@ -39,6 +42,39 @@ export namespace $abi {
|
|
|
39
42
|
let outputs = typeof abi === 'string'
|
|
40
43
|
? ($abiParser.parseMethod(abi).outputs ?? [])
|
|
41
44
|
: (Array.isArray(abi) ? abi : abi.outputs ?? []);
|
|
45
|
+
try {
|
|
46
|
+
return decodeReturnInner(outputs, hex);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
if (outputs.length > 1) {
|
|
49
|
+
// Zero-pad missing trailing outputs only when all outputs are static
|
|
50
|
+
// and the data ends at an output boundary.
|
|
51
|
+
// Supports newer ABIs with trailing outputs that an older deployed contract does not return.
|
|
52
|
+
const receivedWords = $hex.getBytesLength(hex) / 32;
|
|
53
|
+
const abiWords = alot(outputs).sum($types.wordSizeOf);
|
|
54
|
+
if (Number.isFinite(abiWords)
|
|
55
|
+
&& Number.isInteger(receivedWords)
|
|
56
|
+
&& receivedWords > 0
|
|
57
|
+
&& receivedWords < abiWords
|
|
58
|
+
) {
|
|
59
|
+
let missingWords = abiWords - receivedWords;
|
|
60
|
+
let boundaryWords = missingWords;
|
|
61
|
+
for (let i = outputs.length - 1; i > -1; i--) {
|
|
62
|
+
boundaryWords -= $types.wordSizeOf(outputs[i]);
|
|
63
|
+
if (boundaryWords <= 0) {
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (boundaryWords == 0) {
|
|
68
|
+
let expanded = hex + '0'.repeat(64 * missingWords);
|
|
69
|
+
return decodeReturnInner(outputs, expanded);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function decodeReturnInner<T = any>(outputs: TAbiOutput[], hex: string): T {
|
|
42
78
|
let returnAbi = outputs;
|
|
43
79
|
let isDynamic: boolean = null;
|
|
44
80
|
if (outputs.length > 1) {
|
|
@@ -302,9 +302,9 @@ export class ContractReader implements IContractReader {
|
|
|
302
302
|
return filters as RpcTypes.Filter;
|
|
303
303
|
}
|
|
304
304
|
|
|
305
|
-
static async read <TResult = any> (client: Web3Client, address: TAddress, methodAbi: string){
|
|
305
|
+
static async read <TResult = any> (client: Web3Client, address: TAddress, methodAbi: string, ...params){
|
|
306
306
|
let reader = new ContractReader(client);
|
|
307
|
-
return reader.readAsync<TResult>(address, methodAbi);
|
|
307
|
+
return reader.readAsync<TResult>(address, methodAbi, ...params);
|
|
308
308
|
}
|
|
309
309
|
}
|
|
310
310
|
|
package/src/models/TAccount.ts
CHANGED
|
@@ -4,7 +4,12 @@ import { TEth } from './TEth';
|
|
|
4
4
|
import { TPlatform } from './TPlatform';
|
|
5
5
|
|
|
6
6
|
export interface IAccount {
|
|
7
|
-
type?: 'eoa'
|
|
7
|
+
type?: 'eoa'
|
|
8
|
+
| 'safe'
|
|
9
|
+
| 'erc4337'
|
|
10
|
+
| 'timelock'
|
|
11
|
+
| 'impersonated'
|
|
12
|
+
| 'zodiac'
|
|
8
13
|
name?: string
|
|
9
14
|
platform?: TPlatform
|
|
10
15
|
|
|
@@ -68,4 +73,14 @@ export interface TimelockAccount extends IAccount {
|
|
|
68
73
|
operator: IAccount
|
|
69
74
|
}
|
|
70
75
|
|
|
71
|
-
export
|
|
76
|
+
export interface ZodiacAccount extends IAccount {
|
|
77
|
+
type: 'zodiac'
|
|
78
|
+
platform: TPlatform
|
|
79
|
+
/** Zodiac Roles Modifier address, not the Safe/avatar address. */
|
|
80
|
+
address: TAddress
|
|
81
|
+
operator: EoAccount
|
|
82
|
+
/** Roles v2 bytes32 role key. */
|
|
83
|
+
roleKey: TEth.Hex
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export type TAccount = string | IAccount | EoAccount | SafeAccount | Erc4337Account | ZodiacAccount;
|
package/src/models/TEth.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ZodiacAccount as TZodiacAccount } from './TAccount';
|
|
1
2
|
import type { RpcTypes } from 'dequanto/rpc/Rpc'
|
|
2
3
|
import { TPlatform } from './TPlatform'
|
|
3
4
|
|
|
@@ -135,7 +136,7 @@ export namespace TEth {
|
|
|
135
136
|
|
|
136
137
|
|
|
137
138
|
export interface IAccount {
|
|
138
|
-
type?: 'eoa' | 'safe' | 'erc4337' | 'timelock' | 'impersonated'
|
|
139
|
+
type?: 'eoa' | 'safe' | 'erc4337' | 'timelock' | 'impersonated' | 'zodiac'
|
|
139
140
|
name?: string
|
|
140
141
|
address?: Address
|
|
141
142
|
platform?: Platform
|
|
@@ -154,6 +155,8 @@ export namespace TEth {
|
|
|
154
155
|
personal_sign (challenge: string, address: TEth.Address): Promise<TEth.Hex>
|
|
155
156
|
}
|
|
156
157
|
|
|
158
|
+
export interface ZodiacAccount extends TZodiacAccount {}
|
|
159
|
+
|
|
157
160
|
export interface SafeAccount extends IAccount {
|
|
158
161
|
type: 'safe'
|
|
159
162
|
provider?: 'gnosis'
|
|
@@ -45,10 +45,10 @@ export class GnosisSafeHandler {
|
|
|
45
45
|
this.client = config.client ?? di.resolve(EthWeb3Client);
|
|
46
46
|
this.transport = config.transport ?? (
|
|
47
47
|
config.owners.length > 1
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
// when multiple owners are provided assume the transport is InMemory
|
|
49
|
+
? new InMemoryServiceTransport(this.client, this.owners[0])
|
|
50
|
+
// otherwise use the default Safe Transaction Service
|
|
51
|
+
: new SafeServiceTransport(this.client, this.owners)
|
|
52
52
|
)
|
|
53
53
|
|
|
54
54
|
$require.Address(this.safeAddress, `Safe address ${this.safeAddress} is not valid`);
|
|
@@ -147,7 +147,7 @@ export class GnosisSafeHandler {
|
|
|
147
147
|
return tx;
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
-
async executeTxData
|
|
150
|
+
async executeTxData(txData: TEth.TxLike, owner: EoAccount, safeTxParams?: {
|
|
151
151
|
// 0 - Call (default)
|
|
152
152
|
// 1 - DelegateCall
|
|
153
153
|
operation?: 0 | 1
|
|
@@ -158,7 +158,7 @@ export class GnosisSafeHandler {
|
|
|
158
158
|
return tx;
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
-
async createTxHash
|
|
161
|
+
async createTxHash(builder: TxDataBuilder, value?: bigint, safeTxParams?: {
|
|
162
162
|
operation?: 0 | 1
|
|
163
163
|
}) {
|
|
164
164
|
let txData = builder.getTxData(this.client);
|
|
@@ -234,7 +234,7 @@ export class GnosisSafeHandler {
|
|
|
234
234
|
let txProps: SafeServiceTypes.ProposeTransactionProps = {
|
|
235
235
|
safeAddress: $address.toChecksum(this.safeAddress),
|
|
236
236
|
senderAddress: $address.toChecksum(owner.address),
|
|
237
|
-
safeTransaction: <SafeServiceTypes.SafeTransaction>
|
|
237
|
+
safeTransaction: <SafeServiceTypes.SafeTransaction>{
|
|
238
238
|
data: safeTxData,
|
|
239
239
|
signatures: signatures,
|
|
240
240
|
},
|
|
@@ -286,11 +286,11 @@ export class GnosisSafeHandler {
|
|
|
286
286
|
})
|
|
287
287
|
}
|
|
288
288
|
|
|
289
|
-
static parseSafeTx(buffer: TEth.Hex, value?): { name, args: any[]} {
|
|
290
|
-
return $abiUtils.parseMethodCallData([
|
|
289
|
+
static parseSafeTx(buffer: TEth.Hex, value?): { name, args: any[] } {
|
|
290
|
+
return $abiUtils.parseMethodCallData([SafeAbi.execTransaction], { input: buffer, value });
|
|
291
291
|
}
|
|
292
292
|
|
|
293
|
-
static async getInstance
|
|
293
|
+
static async getInstance(senderMix: string | EoAccount, safeAccount: SafeAccount, client: Web3Client, options: {
|
|
294
294
|
transport?: ISafeServiceTransport
|
|
295
295
|
contracts?: typeof config.safe.contracts
|
|
296
296
|
}): Promise<GnosisSafeHandler> {
|
|
@@ -298,7 +298,7 @@ export class GnosisSafeHandler {
|
|
|
298
298
|
? await ChainAccountService.get(senderMix) as EoAccount
|
|
299
299
|
: senderMix;
|
|
300
300
|
|
|
301
|
-
let owners = [
|
|
301
|
+
let owners = [sender];
|
|
302
302
|
|
|
303
303
|
if (safeAccount.owners) {
|
|
304
304
|
let others = await alot(safeAccount.owners)
|
|
@@ -408,7 +408,7 @@ const SafeAbi = {
|
|
|
408
408
|
}
|
|
409
409
|
]
|
|
410
410
|
},
|
|
411
|
-
getTransactionHash: <TAbiItem>
|
|
411
|
+
getTransactionHash: <TAbiItem>{
|
|
412
412
|
"type": "function",
|
|
413
413
|
"stateMutability": "view",
|
|
414
414
|
"outputs": [
|
|
@@ -86,10 +86,10 @@ export class TimelockService implements ITimelockService {
|
|
|
86
86
|
let [ callSalt ] = callSaltArr;
|
|
87
87
|
|
|
88
88
|
let [ callScheduleFirst ] = callScheduled;
|
|
89
|
-
callScheduleFirst.params.delay
|
|
90
89
|
|
|
91
90
|
let schedule = {
|
|
92
91
|
sender: null,
|
|
92
|
+
timelock: callScheduleFirst.address,
|
|
93
93
|
key: callSalt.params.id,
|
|
94
94
|
id: callSalt.params.id,
|
|
95
95
|
salt: callSalt.params.salt,
|
|
@@ -2,6 +2,7 @@ import { $abiParser } from 'dequanto/utils/$abiParser';
|
|
|
2
2
|
import { $abiType } from 'dequanto/utils/$abiType';
|
|
3
3
|
import { $abiUtils } from 'dequanto/utils/$abiUtils';
|
|
4
4
|
import alot from 'alot';
|
|
5
|
+
import { type TAbiInput } from 'dequanto/types/TAbi';
|
|
5
6
|
|
|
6
7
|
export namespace $types {
|
|
7
8
|
export function isFixedArray (type: string) {
|
|
@@ -23,6 +24,11 @@ export namespace $types {
|
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Returns the type size in bits, excluding ABI padding.
|
|
29
|
+
* Sums element sizes for fixed arrays and tuple declarations.
|
|
30
|
+
* Returns Infinity for dynamic or unknown types.
|
|
31
|
+
*/
|
|
26
32
|
export function sizeOf(type: string) {
|
|
27
33
|
if (type === 'address') {
|
|
28
34
|
// 20bytes
|
|
@@ -58,4 +64,25 @@ export namespace $types {
|
|
|
58
64
|
}
|
|
59
65
|
return Infinity;
|
|
60
66
|
}
|
|
67
|
+
|
|
68
|
+
/** Number of 32-byte ABI words; Infinity for dynamic or unknown types. */
|
|
69
|
+
export function wordSizeOf(type: string | TAbiInput): number {
|
|
70
|
+
let input = typeof type === 'string' ? { type } : type;
|
|
71
|
+
let name = input.type.trim();
|
|
72
|
+
if (isFixedArray(name)) {
|
|
73
|
+
let baseType = $abiType.array.getBaseType(name);
|
|
74
|
+
let words = wordSizeOf({ ...input, type: baseType });
|
|
75
|
+
return Number.isFinite(words) ? words * $abiType.array.getLength(name) : Infinity;
|
|
76
|
+
}
|
|
77
|
+
if (isDynamicArray(name)) {
|
|
78
|
+
return Infinity;
|
|
79
|
+
}
|
|
80
|
+
if (name === 'tuple') {
|
|
81
|
+
return input.components == null ? Infinity : alot(input.components).sum(x => wordSizeOf(x));
|
|
82
|
+
}
|
|
83
|
+
if (isStruct(name) || name === '()') {
|
|
84
|
+
return alot($abiParser.parseArguments(name)).sum(x => wordSizeOf(x));
|
|
85
|
+
}
|
|
86
|
+
return Math.ceil(sizeOf(name) / 256);
|
|
87
|
+
}
|
|
61
88
|
}
|
package/src/txs/TxWriter.ts
CHANGED
|
@@ -673,10 +673,12 @@ export class TxWriter extends class_EventEmitter<ITxWriterEvents> implements ITx
|
|
|
673
673
|
let account = await TxWriter.prepareAccount(accountMix);
|
|
674
674
|
let txBuilder = new TxDataBuilder(client, account, data);
|
|
675
675
|
|
|
676
|
-
await
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
676
|
+
if (await TxWriter.hasAgent(account) === false) {
|
|
677
|
+
await Promise.all([
|
|
678
|
+
txBuilder.ensureGas(),
|
|
679
|
+
txBuilder.ensureNonce(),
|
|
680
|
+
]);
|
|
681
|
+
}
|
|
680
682
|
|
|
681
683
|
let w = new TxWriter(client, txBuilder, account);
|
|
682
684
|
w.send(options);
|