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.
Files changed (98) hide show
  1. package/lib/cjs/abi/$abi.js +39 -1
  2. package/lib/cjs/abi/$abi.js.map +1 -1
  3. package/lib/cjs/contracts/ContractReader.js +2 -2
  4. package/lib/cjs/contracts/ContractReader.js.map +1 -1
  5. package/lib/cjs/safe/GnosisSafeHandler.js.map +1 -1
  6. package/lib/cjs/services/TimelockService/ITimelockService.js.map +1 -1
  7. package/lib/cjs/services/TimelockService/TimelockService.js +1 -1
  8. package/lib/cjs/services/TimelockService/TimelockService.js.map +1 -1
  9. package/lib/cjs/solidity/utils/$types.js +26 -0
  10. package/lib/cjs/solidity/utils/$types.js.map +1 -1
  11. package/lib/cjs/txs/TxWriter.js +6 -4
  12. package/lib/cjs/txs/TxWriter.js.map +1 -1
  13. package/lib/cjs/txs/agents/BatchAgent.js +32 -7
  14. package/lib/cjs/txs/agents/BatchAgent.js.map +1 -1
  15. package/lib/cjs/txs/agents/TxWriterAccountAgents.js +2 -0
  16. package/lib/cjs/txs/agents/TxWriterAccountAgents.js.map +1 -1
  17. package/lib/cjs/txs/agents/ZodiacAgent.js +20 -0
  18. package/lib/cjs/txs/agents/ZodiacAgent.js.map +1 -0
  19. package/lib/cjs/utils/$account.js +5 -1
  20. package/lib/cjs/utils/$account.js.map +1 -1
  21. package/lib/cjs/zodiac/ZodiacTx.js +83 -0
  22. package/lib/cjs/zodiac/ZodiacTx.js.map +1 -0
  23. package/lib/esm/abi/$abi.js.map +1 -1
  24. package/lib/esm/abi/$abi.mjs +36 -1
  25. package/lib/esm/contracts/ContractReader.js.map +1 -1
  26. package/lib/esm/contracts/ContractReader.mjs +2 -2
  27. package/lib/esm/safe/GnosisSafeHandler.js.map +1 -1
  28. package/lib/esm/services/TimelockService/ITimelockService.js.map +1 -1
  29. package/lib/esm/services/TimelockService/TimelockService.js.map +1 -1
  30. package/lib/esm/services/TimelockService/TimelockService.mjs +1 -1
  31. package/lib/esm/solidity/utils/$types.js.map +1 -1
  32. package/lib/esm/solidity/utils/$types.mjs +26 -0
  33. package/lib/esm/txs/TxWriter.js.map +1 -1
  34. package/lib/esm/txs/TxWriter.mjs +6 -4
  35. package/lib/esm/txs/agents/BatchAgent.js.map +1 -1
  36. package/lib/esm/txs/agents/BatchAgent.mjs +32 -7
  37. package/lib/esm/txs/agents/TxWriterAccountAgents.js.map +1 -1
  38. package/lib/esm/txs/agents/TxWriterAccountAgents.mjs +2 -0
  39. package/lib/esm/txs/agents/ZodiacAgent.js.map +1 -0
  40. package/lib/esm/txs/agents/ZodiacAgent.mjs +16 -0
  41. package/lib/esm/utils/$account.js.map +1 -1
  42. package/lib/esm/utils/$account.mjs +5 -1
  43. package/lib/esm/zodiac/ZodiacTx.js.map +1 -0
  44. package/lib/esm/zodiac/ZodiacTx.mjs +79 -0
  45. package/lib/types/contracts/ContractReader.d.ts +1 -1
  46. package/lib/types/models/TAccount.d.ts +11 -2
  47. package/lib/types/models/TEth.d.ts +4 -1
  48. package/lib/types/services/TimelockService/ITimelockService.d.ts +1 -0
  49. package/lib/types/solidity/utils/$types.d.ts +8 -0
  50. package/lib/types/txs/agents/BatchAgent.d.ts +2 -1
  51. package/lib/types/txs/agents/ZodiacAgent.d.ts +7 -0
  52. package/lib/types/utils/$account.d.ts +2 -1
  53. package/lib/types/zodiac/ZodiacTx.d.ts +30 -0
  54. package/package.json +1 -1
  55. package/skills/dequanto/INDEX.md +1 -1
  56. package/skills/dequanto/SKILL.md +4 -3
  57. package/skills/dequanto/examples/browser-wallet.spec.ts +2 -1
  58. package/skills/dequanto/examples/config.spec.ts +2 -1
  59. package/skills/dequanto/examples/contract-read.spec.ts +2 -1
  60. package/skills/dequanto/examples/contract-write.spec.ts +2 -1
  61. package/skills/dequanto/examples/deploy-contract.spec.ts +2 -1
  62. package/skills/dequanto/examples/erc4337-userop.spec.ts +2 -1
  63. package/skills/dequanto/examples/explorer-abi.spec.ts +2 -1
  64. package/skills/dequanto/examples/generate-contract-class.spec.ts +2 -1
  65. package/skills/dequanto/examples/hardhat-debug.spec.ts +2 -1
  66. package/skills/dequanto/examples/hardhat-fork.spec.ts +2 -1
  67. package/skills/dequanto/examples/index-events.spec.ts +2 -1
  68. package/skills/dequanto/examples/live-events.spec.ts +2 -1
  69. package/skills/dequanto/examples/revert-error.spec.ts +2 -1
  70. package/skills/dequanto/examples/rpc-read.spec.ts +19 -5
  71. package/skills/dequanto/examples/safe-batch.spec.ts +92 -3
  72. package/skills/dequanto/examples/send-transaction.spec.ts +2 -1
  73. package/skills/dequanto/examples/storage.spec.ts +2 -1
  74. package/skills/dequanto/examples/token-transfer.spec.ts +2 -1
  75. package/skills/dequanto/examples/utils.spec.ts +2 -1
  76. package/skills/dequanto/references/safe-erc4337.md +50 -3
  77. package/skills/dequanto/references/tokens-accounts.md +1 -0
  78. package/skills/dequanto/references/transactions.md +2 -1
  79. package/src/abi/$abi.ts +36 -0
  80. package/src/contracts/ContractReader.ts +2 -2
  81. package/src/models/TAccount.ts +17 -2
  82. package/src/models/TEth.ts +4 -1
  83. package/src/safe/GnosisSafeHandler.ts +12 -12
  84. package/src/services/TimelockService/ITimelockService.ts +2 -0
  85. package/src/services/TimelockService/TimelockService.ts +1 -1
  86. package/src/solidity/utils/$types.ts +27 -0
  87. package/src/txs/TxWriter.ts +6 -4
  88. package/src/txs/agents/BatchAgent.ts +33 -7
  89. package/src/txs/agents/TxWriterAccountAgents.ts +2 -0
  90. package/src/txs/agents/ZodiacAgent.ts +19 -0
  91. package/src/utils/$account.ts +6 -2
  92. package/src/zodiac/ZodiacTx.ts +99 -0
  93. package/typings/index.d.ts +1 -4
  94. package/typings/globals/atma-utest/index.d.ts +0 -53
  95. package/typings/globals/atma-utest/typings.json +0 -10
  96. package/typings/globals/axios/index.d.ts +0 -6
  97. package/typings/globals/mask/index.d.ts +0 -6
  98. 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
- $require.eq(blockNumber, 24576295);
55
- $require.eq(blockNumber, 24576295);
56
- $require.lte(Math.abs($date.toUnixTimestamp(date) - info.timestamp), 1);
57
- $require.eq(blockDate.toISOString(), new Date('2026-03-03T10:19:59Z').toISOString());
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 'build a Safe batch from generated tx data' () {
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
 
@@ -4,7 +4,12 @@ import { TEth } from './TEth';
4
4
  import { TPlatform } from './TPlatform';
5
5
 
6
6
  export interface IAccount {
7
- type?: 'eoa' | 'safe' | 'erc4337' | 'timelock' | 'impersonated'
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 type TAccount = string | IAccount | EoAccount | SafeAccount | Erc4337Account;
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;
@@ -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
- // 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)
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 (txData: TEth.TxLike, owner: EoAccount, safeTxParams?: {
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 (builder: TxDataBuilder, value?: bigint, safeTxParams?: {
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([ SafeAbi.execTransaction ], { input: buffer, value });
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 (senderMix: string | EoAccount, safeAccount: SafeAccount, client: Web3Client, options: {
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 = [ sender ];
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": [
@@ -67,6 +67,8 @@ export interface ITimelockService {
67
67
  }
68
68
 
69
69
  export interface ITimelockTx {
70
+ timelock?: TEth.Address
71
+
70
72
  // operation hash, unique as includes random salt
71
73
  id: TEth.Hex
72
74
  // operation key, should be unique only within pending operations
@@ -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
  }
@@ -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 Promise.all([
677
- txBuilder.ensureGas(),
678
- txBuilder.ensureNonce(),
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);