dequanto 0.2.53 → 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 (110) 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/gen/Generator.js +1 -0
  6. package/lib/cjs/gen/Generator.js.map +1 -1
  7. package/lib/cjs/gen/GeneratorFromAbi.js +38 -9
  8. package/lib/cjs/gen/GeneratorFromAbi.js.map +1 -1
  9. package/lib/cjs/safe/GnosisSafeHandler.js.map +1 -1
  10. package/lib/cjs/services/TimelockService/ITimelockService.js.map +1 -1
  11. package/lib/cjs/services/TimelockService/TimelockService.js +1 -1
  12. package/lib/cjs/services/TimelockService/TimelockService.js.map +1 -1
  13. package/lib/cjs/solidity/utils/$types.js +26 -0
  14. package/lib/cjs/solidity/utils/$types.js.map +1 -1
  15. package/lib/cjs/txs/TxWriter.js +6 -4
  16. package/lib/cjs/txs/TxWriter.js.map +1 -1
  17. package/lib/cjs/txs/agents/BatchAgent.js +32 -7
  18. package/lib/cjs/txs/agents/BatchAgent.js.map +1 -1
  19. package/lib/cjs/txs/agents/TxWriterAccountAgents.js +2 -0
  20. package/lib/cjs/txs/agents/TxWriterAccountAgents.js.map +1 -1
  21. package/lib/cjs/txs/agents/ZodiacAgent.js +20 -0
  22. package/lib/cjs/txs/agents/ZodiacAgent.js.map +1 -0
  23. package/lib/cjs/utils/$account.js +5 -1
  24. package/lib/cjs/utils/$account.js.map +1 -1
  25. package/lib/cjs/zodiac/ZodiacTx.js +83 -0
  26. package/lib/cjs/zodiac/ZodiacTx.js.map +1 -0
  27. package/lib/esm/abi/$abi.js.map +1 -1
  28. package/lib/esm/abi/$abi.mjs +36 -1
  29. package/lib/esm/contracts/ContractReader.js.map +1 -1
  30. package/lib/esm/contracts/ContractReader.mjs +2 -2
  31. package/lib/esm/gen/Generator.js.map +1 -1
  32. package/lib/esm/gen/Generator.mjs +1 -0
  33. package/lib/esm/gen/GeneratorFromAbi.js.map +1 -1
  34. package/lib/esm/gen/GeneratorFromAbi.mjs +38 -9
  35. package/lib/esm/safe/GnosisSafeHandler.js.map +1 -1
  36. package/lib/esm/services/TimelockService/ITimelockService.js.map +1 -1
  37. package/lib/esm/services/TimelockService/TimelockService.js.map +1 -1
  38. package/lib/esm/services/TimelockService/TimelockService.mjs +1 -1
  39. package/lib/esm/solidity/utils/$types.js.map +1 -1
  40. package/lib/esm/solidity/utils/$types.mjs +26 -0
  41. package/lib/esm/txs/TxWriter.js.map +1 -1
  42. package/lib/esm/txs/TxWriter.mjs +6 -4
  43. package/lib/esm/txs/agents/BatchAgent.js.map +1 -1
  44. package/lib/esm/txs/agents/BatchAgent.mjs +32 -7
  45. package/lib/esm/txs/agents/TxWriterAccountAgents.js.map +1 -1
  46. package/lib/esm/txs/agents/TxWriterAccountAgents.mjs +2 -0
  47. package/lib/esm/txs/agents/ZodiacAgent.js.map +1 -0
  48. package/lib/esm/txs/agents/ZodiacAgent.mjs +16 -0
  49. package/lib/esm/utils/$account.js.map +1 -1
  50. package/lib/esm/utils/$account.mjs +5 -1
  51. package/lib/esm/zodiac/ZodiacTx.js.map +1 -0
  52. package/lib/esm/zodiac/ZodiacTx.mjs +79 -0
  53. package/lib/types/contracts/ContractReader.d.ts +1 -1
  54. package/lib/types/gen/Generator.d.ts +17 -0
  55. package/lib/types/gen/GeneratorFromAbi.d.ts +7 -0
  56. package/lib/types/models/TAccount.d.ts +11 -2
  57. package/lib/types/models/TEth.d.ts +4 -1
  58. package/lib/types/services/TimelockService/ITimelockService.d.ts +1 -0
  59. package/lib/types/solidity/utils/$types.d.ts +8 -0
  60. package/lib/types/txs/agents/BatchAgent.d.ts +2 -1
  61. package/lib/types/txs/agents/ZodiacAgent.d.ts +7 -0
  62. package/lib/types/utils/$account.d.ts +2 -1
  63. package/lib/types/zodiac/ZodiacTx.d.ts +30 -0
  64. package/package.json +1 -1
  65. package/skills/dequanto/INDEX.md +1 -1
  66. package/skills/dequanto/SKILL.md +4 -3
  67. package/skills/dequanto/examples/browser-wallet.spec.ts +2 -1
  68. package/skills/dequanto/examples/config.spec.ts +2 -1
  69. package/skills/dequanto/examples/contract-read.spec.ts +2 -1
  70. package/skills/dequanto/examples/contract-write.spec.ts +2 -1
  71. package/skills/dequanto/examples/deploy-contract.spec.ts +2 -1
  72. package/skills/dequanto/examples/erc4337-userop.spec.ts +2 -1
  73. package/skills/dequanto/examples/explorer-abi.spec.ts +2 -1
  74. package/skills/dequanto/examples/generate-contract-class.spec.ts +2 -1
  75. package/skills/dequanto/examples/hardhat-debug.spec.ts +2 -1
  76. package/skills/dequanto/examples/hardhat-fork.spec.ts +2 -1
  77. package/skills/dequanto/examples/index-events.spec.ts +2 -1
  78. package/skills/dequanto/examples/live-events.spec.ts +2 -1
  79. package/skills/dequanto/examples/revert-error.spec.ts +2 -1
  80. package/skills/dequanto/examples/rpc-read.spec.ts +19 -5
  81. package/skills/dequanto/examples/safe-batch.spec.ts +92 -3
  82. package/skills/dequanto/examples/send-transaction.spec.ts +2 -1
  83. package/skills/dequanto/examples/storage.spec.ts +2 -1
  84. package/skills/dequanto/examples/token-transfer.spec.ts +2 -1
  85. package/skills/dequanto/examples/utils.spec.ts +2 -1
  86. package/skills/dequanto/references/safe-erc4337.md +50 -3
  87. package/skills/dequanto/references/tokens-accounts.md +1 -0
  88. package/skills/dequanto/references/transactions.md +2 -1
  89. package/src/abi/$abi.ts +36 -0
  90. package/src/contracts/ContractReader.ts +2 -2
  91. package/src/gen/Generator.ts +4 -0
  92. package/src/gen/GeneratorFromAbi.ts +40 -10
  93. package/src/models/TAccount.ts +17 -2
  94. package/src/models/TEth.ts +4 -1
  95. package/src/safe/GnosisSafeHandler.ts +12 -12
  96. package/src/services/TimelockService/ITimelockService.ts +2 -0
  97. package/src/services/TimelockService/TimelockService.ts +1 -1
  98. package/src/solidity/utils/$types.ts +27 -0
  99. package/src/txs/TxWriter.ts +6 -4
  100. package/src/txs/agents/BatchAgent.ts +33 -7
  101. package/src/txs/agents/TxWriterAccountAgents.ts +2 -0
  102. package/src/txs/agents/ZodiacAgent.ts +19 -0
  103. package/src/utils/$account.ts +6 -2
  104. package/src/zodiac/ZodiacTx.ts +99 -0
  105. package/typings/index.d.ts +1 -4
  106. package/typings/globals/atma-utest/index.d.ts +0 -53
  107. package/typings/globals/atma-utest/typings.json +0 -10
  108. package/typings/globals/axios/index.d.ts +0 -6
  109. package/typings/globals/mask/index.d.ts +0 -6
  110. package/typings/globals/mask/typings.json +0 -9
@@ -1,7 +1,8 @@
1
+ import { UTest } from 'atma-utest';
1
2
  import { Web3ClientFactory } from 'dequanto/clients/Web3ClientFactory';
2
3
  import { $require } from 'dequanto/utils/$require';
3
4
 
4
- UTest({
5
+ UTest.create({
5
6
  async 'change balances, storage, and time on Hardhat and roll back'() {
6
7
  const client = await Web3ClientFactory.getAsync('hh:memory');
7
8
  const address = '0x0000000000000000000000000000000000001234';
@@ -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
 
@@ -56,6 +56,9 @@ export interface IGenerateOptions {
56
56
  */
57
57
  implementation?: TEth.Hex | TEth.Address | string
58
58
 
59
+ /** When false: no files will be saved. Use "files" in result */
60
+ save?: boolean
61
+
59
62
  /** ABI will be save alongside with TS classes */
60
63
  saveAbi?: boolean
61
64
 
@@ -240,6 +243,7 @@ export class Generator {
240
243
  implementation: implementation,
241
244
  sources: sources?.files,
242
245
  sourceMain: sourceMain,
246
+ save: this.options.save,
243
247
  saveAbi: this.options.saveAbi,
244
248
  saveSources: this.options.saveSources,
245
249
  client: this.client,
@@ -33,6 +33,8 @@ export class GeneratorFromAbi {
33
33
  output: string
34
34
  outputFileExt?: string
35
35
  implementation: TAddress
36
+ /** No FS writes, use files in response */
37
+ save?: boolean
36
38
  saveAbi?: boolean
37
39
  saveSources?: boolean
38
40
  sources?: {
@@ -320,24 +322,45 @@ export class GeneratorFromAbi {
320
322
  .replace(`/* $METHOD_TYPES$ */`, () => Str.indent(methodTypings.map(x => x.code).join('\n'), ' '))
321
323
  ;
322
324
 
325
+ if (opts.save !== false) {
326
+ await File.writeAsync(outputPath, code, { skipHooks: true });
327
+ }
328
+ const files = [
329
+ {
330
+ kind: 'binding',
331
+ path: outputPath,
332
+ content: code,
333
+ }
334
+ ];
323
335
 
324
- await File.writeAsync(outputPath, code, { skipHooks: true });
325
336
  if (outputSourceType === 'js') {
326
337
  let outputPathDts = outputPath.replace(/\.\w+$/, '.d.ts');
327
- await File.writeAsync(outputPathDts, codeDts, { skipHooks: true });
338
+ if (opts.save !== false) {
339
+ await File.writeAsync(outputPathDts, codeDts, { skipHooks: true });
340
+ }
341
+ files.push({
342
+ kind: 'declaration',
343
+ path: outputPathDts,
344
+ content: codeDts,
345
+ });
328
346
  }
329
347
 
330
348
 
331
- if (opts.saveAbi) {
332
- let path = class_Uri.combine(opts.output, outputDirectory, `${outputFilename}Abi.json`);
333
- await File.writeAsync(path, abiJson);
349
+ let outputPathAbi = class_Uri.combine(opts.output, outputDirectory, `${outputFilename}Abi.json`);
350
+ if (opts.saveAbi && opts.save !== false) {
351
+ await File.writeAsync(outputPathAbi, abiJson);
334
352
  }
353
+ files.push({
354
+ kind: 'abi',
355
+ path: outputPathAbi,
356
+ content: JSON.stringify(abiJson),
357
+ });
335
358
 
336
359
  $logger.log(`bold<green<${className}>> ABI wrapper class created: bold<${outputPath}>`);
337
360
 
338
361
  let sources = opts.sources;
339
362
  let sourceFiles = [];
340
- if (sources && opts.saveSources !== false) {
363
+ if (sources) {
341
364
  sourceFiles = await alot.fromObject(sources).mapAsync(async entry => {
342
365
  let sourceFilename = /\/?([^/]+$)/.exec(entry.key)[1];
343
366
  let sourcePath = entry.key;
@@ -351,9 +374,15 @@ export class GeneratorFromAbi {
351
374
  // sourcePath = `node_modules/${ sourcePath }`;
352
375
  // }
353
376
  let path = class_Uri.combine(opts.output, outputDirectory, outputFilename, sourcePath);
354
- await File.writeAsync(path, entry.value.content, { skipHooks: true });
355
-
356
- $logger.log(`Source code saved: ${path}`);
377
+ if (opts.saveSources !== false && opts.save !== false) {
378
+ await File.writeAsync(path, entry.value.content, { skipHooks: true });
379
+ $logger.log(`Source code saved: ${path}`);
380
+ }
381
+ files.push({
382
+ kind: 'source',
383
+ path: path,
384
+ content: entry.value.content,
385
+ });
357
386
  return path;
358
387
  }).toArrayAsync();
359
388
  }
@@ -363,7 +392,8 @@ export class GeneratorFromAbi {
363
392
  platform: opts.network,
364
393
  address: opts.address,
365
394
  implementation: opts.implementation,
366
- contractName: opts.contractName
395
+ contractName: opts.contractName,
396
+ files
367
397
  };
368
398
  }
369
399
 
@@ -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'