@defisaver/sdk 1.3.36 → 1.3.37-semi-continuous-dev

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 (167) hide show
  1. package/README.md +55 -9
  2. package/docs/DEV.md +63 -0
  3. package/esm/src/Action.js +21 -13
  4. package/esm/src/ActionWithL2.js +4 -0
  5. package/esm/src/Recipe.js +5 -1
  6. package/package.json +2 -6
  7. package/src/Action.ts +18 -9
  8. package/src/ActionWithL2.ts +2 -0
  9. package/src/Recipe.ts +4 -1
  10. package/test/Action.js +83 -3
  11. package/test/ActionWithL2.js +65 -19
  12. package/test/Recipe.js +97 -0
  13. package/tsconfig.json +1 -5
  14. package/umd/index.js +24 -13
  15. package/ACTIONS.md +0 -1765
  16. package/DEV.md +0 -43
  17. package/docs/md/.nojekyll +0 -1
  18. package/docs/md/README.md +0 -12
  19. package/docs/md/classes/AaveBorrowAction.md +0 -458
  20. package/docs/md/classes/AaveClaimStkAaveAction.md +0 -455
  21. package/docs/md/classes/AaveCollateralSwitchAction.md +0 -455
  22. package/docs/md/classes/AavePaybackAction.md +0 -458
  23. package/docs/md/classes/AaveSupplyAction.md +0 -458
  24. package/docs/md/classes/AaveV2FlashLoanAction.md +0 -458
  25. package/docs/md/classes/AaveV2FlashLoanPaybackAction.md +0 -454
  26. package/docs/md/classes/AaveV3ATokenPaybackAction.md +0 -644
  27. package/docs/md/classes/AaveV3BorrowAction.md +0 -606
  28. package/docs/md/classes/AaveV3ClaimRewardsAction.md +0 -603
  29. package/docs/md/classes/AaveV3CollateralSwitchAction.md +0 -603
  30. package/docs/md/classes/AaveV3FlashLoanAction.md +0 -604
  31. package/docs/md/classes/AaveV3FlashLoanPaybackAction.md +0 -454
  32. package/docs/md/classes/AaveV3PaybackAction.md +0 -618
  33. package/docs/md/classes/AaveV3RatioCheckAction.md +0 -454
  34. package/docs/md/classes/AaveV3RatioTrigger.md +0 -454
  35. package/docs/md/classes/AaveV3SetEModeAction.md +0 -601
  36. package/docs/md/classes/AaveV3SupplyAction.md +0 -618
  37. package/docs/md/classes/AaveV3SwapBorrowRateModeAction.md +0 -602
  38. package/docs/md/classes/AaveV3WithdrawAction.md +0 -603
  39. package/docs/md/classes/AaveWithdrawAction.md +0 -456
  40. package/docs/md/classes/Action.md +0 -604
  41. package/docs/md/classes/ActionWithL2.md +0 -612
  42. package/docs/md/classes/AutomationV2Unsub.md +0 -452
  43. package/docs/md/classes/BalancerFlashLoanAction.md +0 -456
  44. package/docs/md/classes/BalancerFlashLoanPaybackAction.md +0 -454
  45. package/docs/md/classes/BalancerV2ClaimAction.md +0 -457
  46. package/docs/md/classes/BalancerV2SupplyAction.md +0 -458
  47. package/docs/md/classes/BalancerV2WithdrawAction.md +0 -470
  48. package/docs/md/classes/CBChickenInAction.md +0 -454
  49. package/docs/md/classes/CBChickenOutAction.md +0 -455
  50. package/docs/md/classes/CBCreateAction.md +0 -454
  51. package/docs/md/classes/CBRedeemAction.md +0 -456
  52. package/docs/md/classes/ChainLinkPriceTrigger.md +0 -453
  53. package/docs/md/classes/ChangeProxyOwnerAction.md +0 -453
  54. package/docs/md/classes/CompV3RatioTrigger.md +0 -454
  55. package/docs/md/classes/CompoundBorrowAction.md +0 -455
  56. package/docs/md/classes/CompoundClaimAction.md +0 -456
  57. package/docs/md/classes/CompoundCollateralSwitchAction.md +0 -454
  58. package/docs/md/classes/CompoundGetDebtAction.md +0 -452
  59. package/docs/md/classes/CompoundPaybackAction.md +0 -456
  60. package/docs/md/classes/CompoundRatioTrigger.md +0 -453
  61. package/docs/md/classes/CompoundSupplyAction.md +0 -456
  62. package/docs/md/classes/CompoundV3AllowAction.md +0 -455
  63. package/docs/md/classes/CompoundV3BorrowAction.md +0 -456
  64. package/docs/md/classes/CompoundV3ClaimAction.md +0 -456
  65. package/docs/md/classes/CompoundV3PaybackAction.md +0 -468
  66. package/docs/md/classes/CompoundV3RatioCheckAction.md +0 -456
  67. package/docs/md/classes/CompoundV3SupplyAction.md +0 -468
  68. package/docs/md/classes/CompoundV3TransferAction.md +0 -457
  69. package/docs/md/classes/CompoundV3WithdrawAction.md +0 -457
  70. package/docs/md/classes/CompoundWithdrawAction.md +0 -455
  71. package/docs/md/classes/ConvexClaimAction.md +0 -466
  72. package/docs/md/classes/ConvexDepositAction.md +0 -468
  73. package/docs/md/classes/ConvexWithdrawAction.md +0 -468
  74. package/docs/md/classes/CurveClaimFeesAction.md +0 -452
  75. package/docs/md/classes/CurveDepositAction.md +0 -467
  76. package/docs/md/classes/CurveGaugeDepositAction.md +0 -455
  77. package/docs/md/classes/CurveGaugeWithdrawAction.md +0 -454
  78. package/docs/md/classes/CurveMintCrvAction.md +0 -452
  79. package/docs/md/classes/CurveStethPoolDepositAction.md +0 -456
  80. package/docs/md/classes/CurveStethPoolWithdrawAction.md +0 -457
  81. package/docs/md/classes/CurveSwapAction.md +0 -457
  82. package/docs/md/classes/CurveWithdrawAction.md +0 -468
  83. package/docs/md/classes/DfsWeb3.md +0 -212
  84. package/docs/md/classes/DyDxFlashLoanAction.md +0 -456
  85. package/docs/md/classes/DyDxFlashLoanPaybackAction.md +0 -454
  86. package/docs/md/classes/DyDxWithdrawAction.md +0 -455
  87. package/docs/md/classes/EulerFlashLoanAction.md +0 -456
  88. package/docs/md/classes/EulerFlashLoanPaybackAction.md +0 -454
  89. package/docs/md/classes/GUniDeposit.md +0 -461
  90. package/docs/md/classes/GUniWithdraw.md +0 -458
  91. package/docs/md/classes/GasFeeAction.md +0 -454
  92. package/docs/md/classes/GasFeeActionL2.md +0 -455
  93. package/docs/md/classes/GasPriceTrigger.md +0 -35
  94. package/docs/md/classes/InstPullTokensAction.md +0 -454
  95. package/docs/md/classes/LidoStakeAction.md +0 -455
  96. package/docs/md/classes/LidoUnwrapAction.md +0 -455
  97. package/docs/md/classes/LidoWrapAction.md +0 -456
  98. package/docs/md/classes/LiquityBorrowAction.md +0 -457
  99. package/docs/md/classes/LiquityClaimAction.md +0 -453
  100. package/docs/md/classes/LiquityClaimSPRewardsAction.md +0 -454
  101. package/docs/md/classes/LiquityClaimStakingRewardsAction.md +0 -454
  102. package/docs/md/classes/LiquityCloseAction.md +0 -454
  103. package/docs/md/classes/LiquityEthGainToTroveAction.md +0 -455
  104. package/docs/md/classes/LiquityOpenAction.md +0 -459
  105. package/docs/md/classes/LiquityPaybackAction.md +0 -456
  106. package/docs/md/classes/LiquityRatioTrigger.md +0 -453
  107. package/docs/md/classes/LiquityRedeemAction.md +0 -461
  108. package/docs/md/classes/LiquitySPDepositAction.md +0 -456
  109. package/docs/md/classes/LiquitySPWithdrawAction.md +0 -458
  110. package/docs/md/classes/LiquityStakeAction.md +0 -456
  111. package/docs/md/classes/LiquitySupplyAction.md +0 -456
  112. package/docs/md/classes/LiquityUnstakeAction.md +0 -458
  113. package/docs/md/classes/LiquityWithdrawAction.md +0 -456
  114. package/docs/md/classes/MStableClaimAction.md +0 -456
  115. package/docs/md/classes/MStableDepositAction.md +0 -461
  116. package/docs/md/classes/MStableWithdrawAction.md +0 -461
  117. package/docs/md/classes/MakerClaimAction.md +0 -455
  118. package/docs/md/classes/MakerFlashLoanAction.md +0 -455
  119. package/docs/md/classes/MakerFlashLoanPaybackAction.md +0 -454
  120. package/docs/md/classes/MakerGenerateAction.md +0 -456
  121. package/docs/md/classes/MakerGiveAction.md +0 -456
  122. package/docs/md/classes/MakerMergeAction.md +0 -455
  123. package/docs/md/classes/MakerOpenVaultAction.md +0 -454
  124. package/docs/md/classes/MakerPaybackAction.md +0 -456
  125. package/docs/md/classes/MakerRatioAction.md +0 -453
  126. package/docs/md/classes/MakerRatioCheckAction.md +0 -457
  127. package/docs/md/classes/MakerRatioTrigger.md +0 -453
  128. package/docs/md/classes/MakerSupplyAction.md +0 -457
  129. package/docs/md/classes/MakerWithdrawAction.md +0 -457
  130. package/docs/md/classes/PullTokenAction.md +0 -455
  131. package/docs/md/classes/RariDepositAction.md +0 -458
  132. package/docs/md/classes/RariWithdrawAction.md +0 -459
  133. package/docs/md/classes/Recipe.md +0 -195
  134. package/docs/md/classes/ReflexerGenerateAction.md +0 -455
  135. package/docs/md/classes/ReflexerNativeUniV2SaviourDepositAction.md +0 -455
  136. package/docs/md/classes/ReflexerNativeUniV2SaviourGetReservesAction.md +0 -454
  137. package/docs/md/classes/ReflexerNativeUniV2SaviourWithdrawAction.md +0 -455
  138. package/docs/md/classes/ReflexerOpenSafeAction.md +0 -453
  139. package/docs/md/classes/ReflexerPaybackAction.md +0 -455
  140. package/docs/md/classes/ReflexerRatioTrigger.md +0 -453
  141. package/docs/md/classes/ReflexerSupplyAction.md +0 -456
  142. package/docs/md/classes/ReflexerWithdrawAction.md +0 -456
  143. package/docs/md/classes/SellAction.md +0 -613
  144. package/docs/md/classes/SendTokenAction.md +0 -467
  145. package/docs/md/classes/SendTokenAndUnwrapAction.md +0 -455
  146. package/docs/md/classes/Strategy.md +0 -195
  147. package/docs/md/classes/SubInputsAction.md +0 -454
  148. package/docs/md/classes/SumInputsAction.md +0 -454
  149. package/docs/md/classes/TimestampTrigger.md +0 -451
  150. package/docs/md/classes/ToggleSubAction.md +0 -454
  151. package/docs/md/classes/TokenBalanceAction.md +0 -452
  152. package/docs/md/classes/TrailingStopTrigger.md +0 -453
  153. package/docs/md/classes/TransferNFTAction.md +0 -454
  154. package/docs/md/classes/UniV3CurrentTickTrigger.md +0 -452
  155. package/docs/md/classes/UniswapSupplyAction.md +0 -461
  156. package/docs/md/classes/UniswapV3CollectAction.md +0 -614
  157. package/docs/md/classes/UniswapV3CreatePoolAction.md +0 -611
  158. package/docs/md/classes/UniswapV3MintAction.md +0 -610
  159. package/docs/md/classes/UniswapV3SupplyAction.md +0 -63
  160. package/docs/md/classes/UniswapV3WithdrawAction.md +0 -618
  161. package/docs/md/classes/UniswapWithdrawAction.md +0 -460
  162. package/docs/md/classes/UnwrapEthAction.md +0 -600
  163. package/docs/md/classes/UpdateSubAction.md +0 -454
  164. package/docs/md/classes/WrapEthAction.md +0 -599
  165. package/docs/md/classes/YearnSupplyAction.md +0 -456
  166. package/docs/md/classes/YearnWithdrawAction.md +0 -456
  167. package/docs/md/modules.md +0 -814
package/README.md CHANGED
@@ -1,16 +1,62 @@
1
- # DeFi Saver Recipe SDK
1
+ # DeFi Saver SDK
2
2
 
3
- Create complex transactions that interact with multiple DeFi protocols.
3
+ Build actions and combine them into recipes for DeFi Saver smart wallets.
4
4
 
5
- ---
5
+ ## Installation
6
6
 
7
- - [General docs](https://docs.defisaver.com/)
8
- - [SDK docs](https://github.com/DecenterApps/defisaver-sdk/blob/master/DEV.md)
9
- - [Available Actions](https://github.com/DecenterApps/defisaver-sdk/blob/master/ACTIONS.md)
10
- - [Pre-made recipes - TODO](https://github.com/DecenterApps/defisaver-sdk/blob/master/RECIPES.md)
7
+ ```sh
8
+ npm install @defisaver/sdk
9
+ ```
10
+
11
+ ## Example
12
+
13
+ Encode a recipe that wraps 0.1 ETH into WETH on Ethereum:
14
+
15
+ ```js
16
+ const { configure, Recipe, actions } = require('@defisaver/sdk');
17
+
18
+ async function main() {
19
+ configure({ chainId: 1, testingMode: false });
20
+
21
+ const recipe = new Recipe('Wrap ETH', [
22
+ new actions.basic.WrapEthAction('100000000000000000'),
23
+ ]);
24
+
25
+ const [target, data] = recipe.encodeForDsProxyCall();
26
+ const value = await recipe.getEthValue();
27
+ console.log({ target, data, value });
28
+ }
29
+
30
+ main().catch(console.error);
31
+ ```
32
+
33
+ Amounts use token base units as decimal strings. Here, `value` is the ETH amount in wei. `target` and `data` are arguments for the smart wallet's `execute(address,bytes)` call; encoding does not submit a transaction.
34
+
35
+ - [Developer guide](docs/DEV.md): recipes, approvals, configuration, and contributing.
36
+ - [Action implementations](src/actions): current constructors and parameter documentation, grouped by protocol.
37
+ - [DeFi Saver documentation](https://docs.defisaver.com/): product and protocol guides.
11
38
 
12
39
  ## Testing
13
40
 
14
- `npm run test` - Run all tests
41
+ From a local checkout, install dependencies with `npm ci`, then run:
42
+
43
+ ```sh
44
+ # Run offline tests (no RPC required)
45
+ npm run test:unit
46
+
47
+ # Run all tests, including RPC-dependent wallet tests
48
+ npm test
49
+
50
+ # Run a single test file (test/Recipe.js)
51
+ npm run test-single --name=Recipe
52
+ ```
53
+
54
+ The full suite requires `RPC` in your environment or a local `.env` file for the wallet tests. Point it at a local or forked test chain.
55
+
56
+ ## Building
57
+
58
+ ```sh
59
+ npm run build
60
+ ```
15
61
 
16
- `npm run test-single --name=your_test_name` - Run single test for specified name e.g. for MyTest.js test name is MyTest
62
+ Builds the package and runs lint with automatic fixes. To check lint without changing files, use `npm run lint-check`.
package/docs/DEV.md ADDED
@@ -0,0 +1,63 @@
1
+ # Developer guide
2
+
3
+ The SDK models a transaction as an `Action`, or a sequence of actions as a `Recipe`. Action constructors describe contract inputs; the shared classes handle encoding and composition. See the [README](../README.md) for installation and a minimal example.
4
+
5
+ ## Configuration and amounts
6
+
7
+ Call `configure({ chainId: 1, testingMode: false })` before constructing actions or recipes. Addresses are resolved during construction. Configuration is global, including the token library's network: avoid switching chains while working with existing objects or concurrent operations.
8
+
9
+ Use decimal strings in token base units: `'1000000'` represents 1 token with 6 decimals, and `'1000000000000000000'` represents 1 token with 18 decimals. Avoid JavaScript numbers for large amounts. Special values such as a maximum amount are action-specific; check the constructor documentation.
10
+
11
+ Supported networks are exported as `networks`. Use `getAddr(name, chainId)` to look up a deployment; it throws when the chain or named deployment is unavailable. Network support does not imply every action is deployed there.
12
+
13
+ ## Composing a recipe
14
+
15
+ This example encodes pulling 1 WETH from an owner into the wallet and returning the amount received to that owner. Replace the example owner with your address when integrating it.
16
+
17
+ ```js
18
+ const { configure, Recipe, actions } = require('@defisaver/sdk');
19
+
20
+ async function main() {
21
+ configure({ chainId: 1, testingMode: false });
22
+
23
+ const owner = '0x0000000000000000000000000000000000000001';
24
+ const weth = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2';
25
+ const recipe = new Recipe('Pull and return WETH', [
26
+ new actions.basic.PullTokenAction(weth, owner, '1000000000000000000'),
27
+ new actions.basic.SendTokenAction(weth, owner, '$1'),
28
+ ]);
29
+
30
+ const approvals = await recipe.getAssetsToApprove();
31
+ const value = await recipe.getEthValue();
32
+ const [target, data] = recipe.encodeForDsProxyCall();
33
+ console.log({ approvals, target, data, value });
34
+ }
35
+
36
+ main().catch(console.error);
37
+ ```
38
+
39
+ `'$1'` refers to the first action's return value, with numbering starting at **1**. References must point to an earlier action and be used in an input that supports mapping. A return value's meaning depends on the action; it is not necessarily a token amount. Nested inputs can have a custom mapping order, defined by the action's `mappableArgs`.
40
+
41
+ `getAssetsToApprove()` describes assets and owners requiring approval. For the ERC-20 example above, the owner approves the executing smart wallet to spend WETH before execution. The returned metadata does not perform approvals or check existing allowances. NFT approvals use different fields; inspect the returned type when supporting them.
42
+
43
+ For a DSProxy, pass `target` and `data` to its `execute(address,bytes)` method and attach `value` to that transaction. Other wallet integrations must use their appropriate execution path. Do not treat the returned target as a standalone user transaction destination.
44
+
45
+ A single action also exposes `encodeForDsProxyCall()`, `getAssetsToApprove()`, and `getEthValue()`. `ActionWithL2` supports specialized packed calldata; callers should normally use the public encoding methods rather than pack bytes themselves.
46
+
47
+ ## Strategies
48
+
49
+ `Strategy` describes reusable automation: add subscription slots with `addSubSlot`, triggers with `addTrigger`, and actions with `addAction`. Strategy inputs can refer to named subscription slots such as `'&vaultId'`, or the special addresses `'&proxy'` and `'&eoa'`. Action return references still use `'$1'`, `'$2'`, and so on.
50
+
51
+ Strategy encoding is separate from recipe execution. See the [strategy example](../test/Strategy.js) and [implementation](../src/Strategy.ts) for the current API.
52
+
53
+ ## Adding an action
54
+
55
+ 1. Add a class under the relevant [protocol directory](../src/actions), extending `Action` or `ActionWithL2`.
56
+ 2. Match the contract's action name, ABI types, and argument order in the `super` call. Define `mappableArgs` in the order expected by the contract.
57
+ 3. Implement approval metadata and ETH value when needed. Implement packed L2 encoding only when the contract supports it.
58
+ 4. Export the class through the protocol index and add its deployments to [addresses.ts](../src/addresses.ts).
59
+ 5. Add tests for encoded values, mappings, approvals, and ETH value, including relevant boundary cases. Keep constructor parameter documentation beside the implementation.
60
+
61
+ [PullTokenAction](../src/actions/basic/PullTokenAction.ts) is a small example. Use existing actions for structure, and the corresponding contract as the source of truth for input semantics.
62
+
63
+ See the [README](../README.md#testing) for testing commands.
package/esm/src/Action.js CHANGED
@@ -12,7 +12,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
12
12
  if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
13
13
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
14
14
  };
15
- var _Action_instances, _Action__getArgumentMappingWithSlots, _Action__getPlaceholderForType;
15
+ var _Action_instances, _Action__getReturnValueReference, _Action__getArgumentMappingWithSlots, _Action__getPlaceholderForType;
16
16
  import AbiCoder from 'web3-eth-abi';
17
17
  import { keccak256, padLeft, toHex } from 'web3-utils';
18
18
  import { CONFIG } from './config';
@@ -52,12 +52,12 @@ export class Action {
52
52
  */
53
53
  _getArgumentMapping() {
54
54
  return this.mappableArgs.map(arg => {
55
- if (new RegExp(/\$\d+/).test(arg)) {
56
- if (Array.isArray(arg))
57
- throw TypeError('Input can\'t be mapped to arrays (tuples/structs). Specify `mappableArgs` array in constructor.');
58
- // eslint-disable-next-line
59
- return parseInt(arg.substr(1));
55
+ if (Array.isArray(arg) && /\$\d+/.test(arg.toString())) {
56
+ throw TypeError('Input can\'t be mapped to arrays (tuples/structs). Specify `mappableArgs` array in constructor.');
60
57
  }
58
+ const reference = __classPrivateFieldGet(this, _Action_instances, "m", _Action__getReturnValueReference).call(this, arg);
59
+ if (reference !== undefined)
60
+ return reference;
61
61
  return 0;
62
62
  });
63
63
  }
@@ -100,7 +100,7 @@ export class Action {
100
100
  if (Array.isArray(arg))
101
101
  return arg.map((_arg, i) => this._replaceWithPlaceholders(_arg, paramTypeParsed[i]));
102
102
  if (typeof (paramType) === 'string') {
103
- if (new RegExp(/\$\d+/).test(arg))
103
+ if (__classPrivateFieldGet(this, _Action_instances, "m", _Action__getReturnValueReference).call(this, arg) !== undefined)
104
104
  return __classPrivateFieldGet(this, _Action_instances, "m", _Action__getPlaceholderForType).call(this, paramType);
105
105
  if (new RegExp(/&\w+/).test(arg))
106
106
  return __classPrivateFieldGet(this, _Action_instances, "m", _Action__getPlaceholderForType).call(this, paramType);
@@ -196,14 +196,22 @@ export class Action {
196
196
  }));
197
197
  }
198
198
  }
199
- _Action_instances = new WeakSet(), _Action__getArgumentMappingWithSlots = function _Action__getArgumentMappingWithSlots(subSlots) {
199
+ _Action_instances = new WeakSet(), _Action__getReturnValueReference = function _Action__getReturnValueReference(arg) {
200
+ if (typeof arg !== 'string' || !/\$\d+/.test(arg))
201
+ return undefined;
202
+ const index = Number(arg.slice(1));
203
+ if (!/^\$\d+$/.test(arg) || !Number.isSafeInteger(index) || index < 1) {
204
+ throw new Error(`Invalid action return reference: ${arg}`);
205
+ }
206
+ return index;
207
+ }, _Action__getArgumentMappingWithSlots = function _Action__getArgumentMappingWithSlots(subSlots) {
200
208
  return this.mappableArgs.map(arg => {
201
- if (new RegExp(/\$\d+/).test(arg)) {
202
- if (Array.isArray(arg))
203
- throw TypeError('Input can\'t be mapped to arrays (tuples/structs). Specify `mappableArgs` array in constructor.');
204
- // eslint-disable-next-line
205
- return parseInt(arg.substr(1));
209
+ if (Array.isArray(arg) && /\$\d+/.test(arg.toString())) {
210
+ throw TypeError('Input can\'t be mapped to arrays (tuples/structs). Specify `mappableArgs` array in constructor.');
206
211
  }
212
+ const reference = __classPrivateFieldGet(this, _Action_instances, "m", _Action__getReturnValueReference).call(this, arg);
213
+ if (reference !== undefined)
214
+ return reference;
207
215
  // Handle SubSlots placeholder values in strategies
208
216
  if (new RegExp(/&\w+/).test(arg)) {
209
217
  if (arg === '&proxy')
@@ -34,10 +34,14 @@ export class ActionWithL2 extends Action {
34
34
  }
35
35
  numberToBytes2(number) {
36
36
  const hexNumber = number.toString(16);
37
+ if (!/^[0-9a-f]{1,4}$/i.test(hexNumber))
38
+ throw new RangeError('Value must fit in an unsigned 2-byte integer');
37
39
  return hexNumber.padStart(4, '0');
38
40
  }
39
41
  numberToBytes1(number) {
40
42
  const hexNumber = number.toString(16);
43
+ if (!/^[0-9a-f]{1,2}$/i.test(hexNumber))
44
+ throw new RangeError('Value must fit in an unsigned 1-byte integer');
41
45
  return hexNumber.padStart(2, '0');
42
46
  }
43
47
  numberToBytes32(number) {
package/esm/src/Recipe.js CHANGED
@@ -146,7 +146,9 @@ export class Recipe {
146
146
  for (const pairsPerAction of assetOwnerPairs) {
147
147
  for (const pair of pairsPerAction) {
148
148
  const isNft = !pair.asset;
149
- if (!uniqueAssetOwnerPairs.find(_pair => _pair.owner === pair.owner && (isNft ? _pair.tokenId === pair.tokenId : _pair.asset === pair.asset))) {
149
+ if (!uniqueAssetOwnerPairs.find(_pair => _pair.owner === pair.owner && (isNft
150
+ ? 'nft' in _pair && _pair.nft === pair.nft && _pair.tokenId === pair.tokenId
151
+ : _pair.asset === pair.asset))) {
150
152
  if (isNft) {
151
153
  uniqueAssetOwnerPairs.push({
152
154
  owner: pair.owner,
@@ -196,6 +198,8 @@ export class Recipe {
196
198
  }
197
199
  }
198
200
  _Recipe_instances = new WeakSet(), _Recipe__encodeForCall = function _Recipe__encodeForCall() {
201
+ if (this.actions.length === 0)
202
+ throw new Error('Cannot encode an empty recipe');
199
203
  const encoded = this.actions.map(action => action.encodeForRecipe());
200
204
  const transposed = encoded[0].map((_, colIndex) => encoded.map(row => row[colIndex]));
201
205
  const taskStruct = [
package/package.json CHANGED
@@ -1,19 +1,18 @@
1
1
  {
2
2
  "name": "@defisaver/sdk",
3
- "version": "1.3.36",
3
+ "version": "1.3.37-semi-continuous-dev",
4
4
  "description": "",
5
5
  "main": "./umd/index.js",
6
6
  "module": "./esm/src/index.js",
7
7
  "types": "./esm/src/index.d.ts",
8
8
  "scripts": {
9
9
  "test": "mocha 'test/**'",
10
+ "test:unit": "mocha 'test/**/*.js' --exclude test/DfsWeb3.js",
10
11
  "test-single": "mocha ./test/$npm_config_name.js",
11
12
  "version-bump": "git commit -am \"Version bump to $(npm version patch | cut -c 2-)\"",
12
13
  "build:esm": "rm -rf esm && tsc -p tsconfig.esm.json",
13
14
  "build:umd": "rm -rf umd && webpack --config=webpack.umd.js",
14
15
  "build": "npm run lint && npm run build:umd && npm run build:esm",
15
- "documentMD": "npx typedoc --plugin typedoc-plugin-markdown --out docs/md",
16
- "document": "npx typedoc --out docs/default",
17
16
  "lint": "eslint . --ext .ts --fix",
18
17
  "lint-check": "eslint .",
19
18
  "build-test": "npm run build && npm run test"
@@ -74,11 +73,8 @@
74
73
  "chai": "^4.2.0",
75
74
  "dotenv": "^10.0.0",
76
75
  "eslint": "^8.57.0",
77
- "jsdoc-to-markdown": "^9.1.3",
78
76
  "mocha": "^11.8.0",
79
77
  "ts-node": "^10.9.1",
80
- "typedoc": "^0.23.20",
81
- "typedoc-plugin-markdown": "^3.13.6",
82
78
  "typescript": "^4.8.4",
83
79
  "webpack": "^5.91.0",
84
80
  "webpack-cli": "^4.10.0"
package/src/Action.ts CHANGED
@@ -49,16 +49,25 @@ export class Action {
49
49
  return keccak256(this.name).substr(0, 10);
50
50
  }
51
51
 
52
+ #_getReturnValueReference(arg: unknown): number | undefined {
53
+ if (typeof arg !== 'string' || !/\$\d+/.test(arg)) return undefined;
54
+ const index = Number(arg.slice(1));
55
+ if (!/^\$\d+$/.test(arg) || !Number.isSafeInteger(index) || index < 1) {
56
+ throw new Error(`Invalid action return reference: ${arg}`);
57
+ }
58
+ return index;
59
+ }
60
+
52
61
  /**
53
62
  *
54
63
  */
55
64
  #_getArgumentMappingWithSlots(subSlots: Args) : Array<number> {
56
65
  return this.mappableArgs.map(arg => {
57
- if (new RegExp(/\$\d+/).test(arg)) {
58
- if (Array.isArray(arg)) throw TypeError('Input can\'t be mapped to arrays (tuples/structs). Specify `mappableArgs` array in constructor.');
59
- // eslint-disable-next-line
60
- return parseInt(arg.substr(1));
66
+ if (Array.isArray(arg) && /\$\d+/.test(arg.toString())) {
67
+ throw TypeError('Input can\'t be mapped to arrays (tuples/structs). Specify `mappableArgs` array in constructor.');
61
68
  }
69
+ const reference = this.#_getReturnValueReference(arg);
70
+ if (reference !== undefined) return reference;
62
71
 
63
72
  // Handle SubSlots placeholder values in strategies
64
73
  if (new RegExp(/&\w+/).test(arg)) {
@@ -77,11 +86,11 @@ export class Action {
77
86
  */
78
87
  _getArgumentMapping() : Args {
79
88
  return this.mappableArgs.map(arg => {
80
- if (new RegExp(/\$\d+/).test(arg)) {
81
- if (Array.isArray(arg)) throw TypeError('Input can\'t be mapped to arrays (tuples/structs). Specify `mappableArgs` array in constructor.');
82
- // eslint-disable-next-line
83
- return parseInt(arg.substr(1));
89
+ if (Array.isArray(arg) && /\$\d+/.test(arg.toString())) {
90
+ throw TypeError('Input can\'t be mapped to arrays (tuples/structs). Specify `mappableArgs` array in constructor.');
84
91
  }
92
+ const reference = this.#_getReturnValueReference(arg);
93
+ if (reference !== undefined) return reference;
85
94
  return 0;
86
95
  });
87
96
  }
@@ -136,7 +145,7 @@ export class Action {
136
145
  const paramTypeParsed = this._parseParamType(paramType, arg);
137
146
  if (Array.isArray(arg)) return arg.map((_arg, i) => this._replaceWithPlaceholders(_arg, paramTypeParsed[i]));
138
147
  if (typeof (paramType) === 'string') {
139
- if (new RegExp(/\$\d+/).test(arg)) return this.#_getPlaceholderForType(paramType);
148
+ if (this.#_getReturnValueReference(arg) !== undefined) return this.#_getPlaceholderForType(paramType);
140
149
  if (new RegExp(/&\w+/).test(arg)) return this.#_getPlaceholderForType(paramType);
141
150
  }
142
151
  return arg;
@@ -31,11 +31,13 @@ export class ActionWithL2 extends Action {
31
31
 
32
32
  numberToBytes2(number:number) {
33
33
  const hexNumber = number.toString(16);
34
+ if (!/^[0-9a-f]{1,4}$/i.test(hexNumber)) throw new RangeError('Value must fit in an unsigned 2-byte integer');
34
35
  return hexNumber.padStart(4, '0');
35
36
  }
36
37
 
37
38
  numberToBytes1(number:number) {
38
39
  const hexNumber = number.toString(16);
40
+ if (!/^[0-9a-f]{1,2}$/i.test(hexNumber)) throw new RangeError('Value must fit in an unsigned 1-byte integer');
39
41
  return hexNumber.padStart(2, '0');
40
42
  }
41
43
 
package/src/Recipe.ts CHANGED
@@ -57,6 +57,7 @@ export class Recipe {
57
57
  *
58
58
  */
59
59
  #_encodeForCall(): Array<string | Array<any>> {
60
+ if (this.actions.length === 0) throw new Error('Cannot encode an empty recipe');
60
61
  const encoded = this.actions.map(action => action.encodeForRecipe());
61
62
  const transposed = encoded[0].map((_, colIndex) => encoded.map(row => row[colIndex]));
62
63
  const taskStruct = [
@@ -170,7 +171,9 @@ export class Recipe {
170
171
  for (const pairsPerAction of assetOwnerPairs) {
171
172
  for (const pair of pairsPerAction) {
172
173
  const isNft = !pair.asset;
173
- if (!uniqueAssetOwnerPairs.find(_pair => _pair.owner === pair.owner && (isNft ? _pair.tokenId === pair.tokenId : _pair.asset === pair.asset))) {
174
+ if (!uniqueAssetOwnerPairs.find(_pair => _pair.owner === pair.owner && (isNft
175
+ ? 'nft' in _pair && _pair.nft === pair.nft && _pair.tokenId === pair.tokenId
176
+ : _pair.asset === pair.asset))) {
174
177
  if (isNft) {
175
178
  uniqueAssetOwnerPairs.push({
176
179
  owner: pair.owner!,
package/test/Action.js CHANGED
@@ -39,7 +39,8 @@ describe('Action', () => {
39
39
  let action;
40
40
  it('Constructor', () => {
41
41
  action = new dfs.Action('MockSwap', '0x0a80C3C540eEF99811f4579fa7b1A0617294e06f', ['uint256', 'address'], ['$1', '0x6b175474e89094c44da98b954eedeac495271d0f']);
42
- action.mappableArgs = [action.args[0][0], action.args[0][1]];
42
+ action.mappableArgs = [action.args[0], action.args[1]];
43
+ assert.deepEqual(action.encodeForRecipe()[3], [1, 0]);
43
44
  })
44
45
  it('encodeForDsProxyCall', () => encodeForDsProxyCall(action));
45
46
  it('encodeForRecipe', () => encodeForRecipe(action));
@@ -55,8 +56,87 @@ describe('Action', () => {
55
56
  assert.throws(() => encodeForRecipe(action));
56
57
  });
57
58
  it('encodeForRecipe with custom mappableArgs', () => {
58
- action.mappableArgs = [action.args[0][0][0], action.args[0][1]];
59
- encodeForRecipe(action)
59
+ action.mappableArgs = [action.args[0][0], action.args[1]];
60
+ assert.deepEqual(action.encodeForRecipe()[3], [1, 0]);
60
61
  });
61
62
  })
62
63
  })
64
+
65
+
66
+ describe('Action encoding', () => {
67
+ const AbiCoder = require('web3-eth-abi');
68
+ const target = '0x0000000000000000000000000000000000000001';
69
+ const recipient = '0x0000000000000000000000000000000000000002';
70
+ const zeroWord = '0'.repeat(64);
71
+
72
+ it('encodes the target, selector, arguments, ID and unmapped inputs', () => {
73
+ const action = new dfs.Action('MockSwap', target, ['uint256', 'address'], ['5', recipient]);
74
+ // Static ABI tuple: one word for the amount, one for the address.
75
+ const payload = `0x${'5'.padStart(64, '0')}${'2'.padStart(64, '0')}`;
76
+ assert.deepEqual(action.encodeForRecipe(), [payload, `0x${zeroWord}`, '0xe45f86c0', [0, 0]]);
77
+ const [address, data] = action.encodeForDsProxyCall();
78
+ assert.equal(address, target);
79
+ assert.equal(data.slice(0, 10), '0x389f87ff'); // executeActionDirect(bytes)
80
+ assert.equal(AbiCoder.decodeParameter('bytes', `0x${data.slice(10)}`), payload);
81
+ });
82
+
83
+ it('replaces mapped inputs with zeros without changing the original arguments', () => {
84
+ const args = ['$2', recipient];
85
+ const action = new dfs.Action('MockSwap', target, ['uint256', 'address'], args);
86
+ const [payload, , , mapping] = action.encodeForRecipe();
87
+ assert.equal(payload, `0x${zeroWord}${'2'.padStart(64, '0')}`);
88
+ assert.deepEqual(mapping, [2, 0]);
89
+ assert.deepEqual(args, ['$2', recipient]);
90
+ });
91
+
92
+ it('preserves nested tuple and array values around mapped inputs', () => {
93
+ const args = [['7', ['$1', recipient]], ['11', '13']];
94
+ const action = new dfs.Action('MockSwap', target,
95
+ [['uint256', ['uint256', 'address']], 'uint256[]'], args);
96
+ action.mappableArgs = [args[0][1][0], args[0][1][1]];
97
+ const [payload, , , mapping] = action.encodeForRecipe();
98
+ const decoded = AbiCoder.decodeParameter('((uint256,(uint256,address)),uint256[])', payload);
99
+ assert.equal(decoded[0][0], '7');
100
+ assert.equal(decoded[0][1][0], '0');
101
+ assert.equal(decoded[0][1][1], recipient);
102
+ assert.deepEqual(decoded[1], ['11', '13']);
103
+ assert.deepEqual(mapping, [1, 0]);
104
+ assert.equal(args[0][1][0], '$1');
105
+ });
106
+
107
+ it('maps strategy slots and special wallet references', () => {
108
+ const action = new dfs.Action('MockSwap', target,
109
+ ['uint256', 'uint256', 'address', 'address', 'uint256'],
110
+ ['$1', '&amount', '&proxy', '&eoa', '9']);
111
+ assert.deepEqual(action.encodeForStrategy({ '&amount': { index: 128 } }),
112
+ ['0xe45f86c0', [1, 128, 254, 255, 0]]);
113
+ });
114
+
115
+ it('rejects mismatched argument counts', () => {
116
+ assert.throws(() => new dfs.Action('MockSwap', target, ['uint256'], []),
117
+ 'Parameters/arguments length mismatch');
118
+ });
119
+ });
120
+
121
+
122
+ describe('Action reference validation', () => {
123
+ const target = '0x0000000000000000000000000000000000000001';
124
+
125
+ for (const reference of ['$0', '$1junk', 'prefix$1', '$1.5', '$9007199254740992']) {
126
+ it(`rejects malformed reference ${reference} in every encoding path`, () => {
127
+ const action = new dfs.Action('MockSwap', target, ['uint256'], [reference]);
128
+ for (const encode of [
129
+ () => action.encodeForDsProxyCall(),
130
+ () => action.encodeForRecipe(),
131
+ () => action.encodeForStrategy({}),
132
+ ]) {
133
+ assert.throws(encode, `Invalid action return reference: ${reference}`);
134
+ }
135
+ });
136
+ }
137
+
138
+ it('preserves existing leading-zero references and ordinary strings', () => {
139
+ const action = new dfs.Action('MockSwap', target, ['uint256', 'string'], ['$01', '$price']);
140
+ assert.deepEqual(action.encodeForRecipe()[3], [1, 0]);
141
+ });
142
+ });
@@ -1,27 +1,73 @@
1
1
  const dfs = require('../src');
2
- const {encodeForDsProxyCall, encodeForRecipe} = require('./_actionUtils');
3
- const {assert} = require('chai');
2
+ const AbiCoder = require('web3-eth-abi');
3
+ const { assert } = require('chai');
4
4
 
5
5
  const MAXUINT = '115792089237316195423570985008687907853269984665640564039457584007913129639935';
6
- const MAXUINT_HEX = 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff';
6
+ const target = '0x0000000000000000000000000000000000000001';
7
+ const recipient = '0x0000000000000000000000000000000000000002';
8
+
7
9
  describe('ActionWithL2', () => {
10
+ it('packs integer boundaries, addresses and booleans to their specified widths', () => {
11
+ const action = new dfs.ActionWithL2('MockSwap', target, [], []);
12
+ for (const [value, expected] of [[0, '00'], [1, '01'], [255, 'ff']]) {
13
+ assert.equal(action.numberToBytes1(value), expected);
14
+ }
15
+ for (const [value, expected] of [[0, '0000'], [1, '0001'], [65535, 'ffff']]) {
16
+ assert.equal(action.numberToBytes2(value), expected);
17
+ }
18
+ assert.equal(action.numberToBytes32('0'), '0'.repeat(64));
19
+ assert.equal(action.numberToBytes32('1'), '1'.padStart(64, '0'));
20
+ assert.equal(action.numberToBytes32(MAXUINT), 'f'.repeat(64));
21
+ assert.equal(action.addressToBytes20(recipient), '2'.padStart(40, '0'));
22
+ assert.equal(action.boolToBytes1(false), '00');
23
+ assert.equal(action.boolToBytes1(true), '01');
24
+ });
8
25
 
9
- context('Basic', () => {
10
- let action;
11
- it('Constructor', () => {
12
- action = new dfs.ActionWithL2('MockSwap', '0x0a80C3C540eEF99811f4579fa7b1A0617294e06f', ['uint256', 'address'], [MAXUINT, '0x6b175474e89094c44da98b954eedeac495271d0f'])
13
- })
14
- it('numberToBytes32', () => {
15
- assert(MAXUINT_HEX === action.numberToBytes32(MAXUINT));
16
- });
26
+ it('falls back to standard ABI encoding when packed encoding is not overridden', () => {
27
+ const action = new dfs.ActionWithL2('MockSwap', target, ['uint256'], ['5']);
28
+ const data = action.encodeForL2DsProxyCall();
29
+ assert.equal(data.slice(0, 10), '0x389f87ff');
30
+ assert.equal(AbiCoder.decodeParameter('bytes', `0x${data.slice(10)}`),
31
+ `0x${'5'.padStart(64, '0')}`);
32
+ assert.equal(action.encodeForL2Recipe(), `0x${data.slice(10)}`);
17
33
  });
18
- context('AaveV3WithdrawAction', () => {
19
- let action;
20
- it('Constructor', () => {
21
- action = new dfs.actions.aaveV3.AaveV3WithdrawAction(true, '0xa97684ead0e402dC232d5A977953DF7ECBaB3CDb', MAXUINT, "0x6b175474e89094c44da98b954eedeac495271d0f", 9);
22
- })
23
- it('numberToBytes32', () => {
24
- assert(action.encodeForL2DsProxyCall() === '0x2895f3aa000901ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff6b175474e89094c44da98b954eedeac495271d0f');
34
+
35
+ for (const useDefaultMarket of [true, false]) {
36
+ it(`encodes Aave withdrawal with ${useDefaultMarket ? 'default' : 'explicit'} market`, () => {
37
+ const action = new dfs.actions.aaveV3.AaveV3WithdrawAction(
38
+ useDefaultMarket, target, MAXUINT, recipient, 9,
39
+ );
40
+ // uint16 asset ID, bool default market, uint256 amount, address recipient,
41
+ // followed by the market address only when not using the default.
42
+ const payload = `0009${useDefaultMarket ? '01' : '00'}${'f'.repeat(64)}${recipient.slice(2)}${useDefaultMarket ? '' : target.slice(2)}`;
43
+ assert.equal(action.encodeForL2DsProxyCall(), `0x2895f3aa${payload}`);
44
+ assert.equal(action.encodeForL2Recipe(), `0x${payload}`);
45
+
46
+ const previous = { ...dfs.CONFIG };
47
+ try {
48
+ dfs.configure({ chainId: 10, testingMode: false });
49
+ assert.deepEqual(action.encodeForDsProxyCall(), [action.contractAddress, `0x2895f3aa${payload}`]);
50
+ } finally {
51
+ dfs.configure(previous);
52
+ }
25
53
  });
54
+ }
55
+ });
56
+
57
+
58
+ describe('ActionWithL2 integer validation', () => {
59
+ const action = new dfs.ActionWithL2('MockSwap', target, [], []);
60
+
61
+ for (const [method, overflow] of [['numberToBytes1', 256], ['numberToBytes2', 65536]]) {
62
+ it(`${method} rejects overflow, negative, fractional and non-finite values`, () => {
63
+ for (const value of [overflow, -1, 1.5, NaN, Infinity]) {
64
+ assert.throws(() => action[method](value), RangeError, 'Value must fit');
65
+ }
66
+ });
67
+ }
68
+
69
+ it('preserves hex text accepted by existing JavaScript callers', () => {
70
+ assert.equal(action.numberToBytes1('10'), '10');
71
+ assert.equal(action.numberToBytes2('10'), '0010');
26
72
  });
27
- })
73
+ });