diff --git a/README.md b/README.md index 08993543..2b5d9bae 100644 --- a/README.md +++ b/README.md @@ -130,9 +130,10 @@ promise.abort() ##### Table of transactions: | **Vault** | **RewardSplitter** | **osToken** | |-------------------------------------------------------------------|---------------------------------------------------------------------------------|-------------------------------------| -| [sdk.vault.deposit](#sdkvaultdeposit) | [sdk.rewardSplitter.create](#sdkrewardsplittercreate) | [sdk.osToken.mint](#sdkostokenmint) | -| [sdk.vault.withdraw](#sdkvaultwithdraw) | [sdk.rewardSplitter.claimRewards](#sdkrewardsplitterclaimrewards) | [sdk.osToken.burn](#sdkostokenburn) | -| [sdk.vault.operate](#sdkvaultoperate) | [sdk.rewardSplitter.updateFeeRecipients](#sdkrewardsplitterupdatefeerecipients) | | +| [sdk.vault.create](#sdkvaultcreate) | [sdk.rewardSplitter.create](#sdkrewardsplittercreate) | [sdk.osToken.mint](#sdkostokenmint) | +| [sdk.vault.deposit](#sdkvaultdeposit) | [sdk.rewardSplitter.claimRewards](#sdkrewardsplitterclaimrewards) | [sdk.osToken.burn](#sdkostokenburn) | +| [sdk.vault.withdraw](#sdkvaultwithdraw) | [sdk.rewardSplitter.updateFeeRecipients](#sdkrewardsplitterupdatefeerecipients) | | +| [sdk.vault.operate](#sdkvaultoperate) | | | | [sdk.vault.setDepositDataManager](#sdkvaultsetdepositdatamanager) | | | | [sdk.vault.setDepositDataRoot](#sdkvaultsetdepositdataroot) | | | | [sdk.claimExitQueue](#sdkvaultclaimexitqueue) | | | @@ -1133,6 +1134,58 @@ await sdk.utils.getTransactions({ hash: '0x...' }) Transactions work through the provider you sent when creating an instance of our SDK class. Or, if you are a custodian, you can get the transaction data and sign it yourself. Each transaction also gives you the opportunity to get an approximate gas for sending it. For custodians, it is more reliable to calculate the gas yourself. Each transaction has encode and estimateGas methods for your convenience +### `sdk.vault.create` + +#### Description: + +Create a vault. When the transaction is executed, one gwei of the deposit token must be stored in the vault to avoid [inflation attack](https://blog.openzeppelin.com/a-novel-defense-against-erc4626-inflation-attacks). +Pay attention to chains where the deposit token is not a native token (such as Gnosis or Chiado). +On these chains before creating the vault, ensure that you call the `approve` function on the deposit token contract, +allowing the vault factory address to spend one gwei. +You can retrieve the vault factory contract using the helper function: `sdk.getVaultFactory({ vaultType: params.type, isErc20: params.isErc20 })`. + + +#### Arguments: + +| Name | Type | Required | Description | +|----------------|------------------------------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------------------| +| userAddress | `string` | **Yes** | The address of the user initiating the action. This address will become the vault admin | +| type | `VaultType` | **No** | Allowed vault types: Default, Private and Blocklist. Available vault types can be found in the `enum VaultType` which you can be imported from the library | +| vaultToken | `{ name: string, symbol: string }` | **No** | If provided, the vault will be created with its own ERC20 token | +| capacity | `bigint` | **No** | If provided, should be defined in gwei. By default, capacity is `MaxUint256`; the minimum allowed capacity is `parseEther('32')` | +| keysManagerFee | `number` | **No** | If provided, should be between `0` and `100`, inclusive with a maximum of two decimal digits allowed (e.g., `15.35`). By default, the fee is `0` | +| isOwnMevEscrow | `boolean` | **No** | Defines whether to send block rewards to the Smoothing Pool (`false`) or keep them only to your Vault (`true`). By default, this value is `false` | +| image | `string` | **No** | The vault image in base64 string format (will be uploaded to IPFS; maximum size is 1 MB) | +| displayName | `string` | **No** | The vault display name (will be uploaded to IPFS; maximum size is 30 characters) | +| description | `string` | **No** | The vault description (will be uploaded to IPFS; maximum size is 1000 characters) | + +#### Example: + +```ts +const params = { + userAddress: '0x...', + type: VaultType.Default, + vaultToken: { + name: 'Vault Token', + symbol: 'vlt', + }, + capacity: MaxUint256, + keysManagerFee: 0, + isOwnMevEscrow: false, + image: 'data:image/png;base64,...', + displayName: 'Example vault', + description: 'Example description', +} + +// Transaction example +// Send transaction to create a vault +const hash = await sdk.vault.create(params) +// When you sign transactions on the backend (for custodians) +const { data, to, value } = await sdk.vault.deposit.encode(params) +// Get an approximate gas per transaction +const gas = await sdk.vault.deposit.estimateGas(params) +``` +--- ### `sdk.vault.deposit` #### Description: @@ -1434,21 +1487,23 @@ Updates the vault by authorized personnel such as the vault admin, whitelistMana #### Arguments: -| Name | Type | Required | Access | Description | -|------------------|----------------------------------------------|----------|-------------------|-----------------------------------------------------------------------------------------------------------------------------| -| whitelistManager | `Array<{ address: string, isNew: boolean }>` | **No** | whitelistManager | List of addresses to update the whitelist. Use `isNew: true` to add a new address, `isNew: false` to remove an existing one. Max count at time - 700 addresses. | -| blocklist | `Array<{ address: string, isNew: boolean }>` | **No** | Blocklist manager | List of addresses to update the blocklist. Use `isNew: true` to add a new address, `isNew: false` to remove an existing one. Max count at time - 700 addresses. | -| depositDataManager | `string` | **No** | Deposit-data manager | Address of the vault keys manager. Support only **first version** on valults. For second verion use `vault.setDepositDataManager` | -| validatorsManager | `string` | **No** | Admin | Address of the vault deposit data manager. Support only **second version** on valults. | -| restakeWithdrawalsManager | `string` | **No** | Admin | The restake withdrawals manager must be assigned to the wallet connected to the operator service. It is responsible for withdrawing exiting validators from the EigenLayer. | -| restakeOperatorsManager | `string` | **No** | Admin | The restake operators manager can add EigenPods and update EigenLayer operators. | -| whitelistManager | `string` | **No** | Admin | Address of the vault whitelistManager | -| feeRecipient | `string` | **No** | Admin | Address of the vault fee recipient | -| validatorsRoot | `string` | **No** | Keys manager | The vault validators merkle tree root. Support only **first version** on valults. For second verion use `vault.setDepositDataRoot` | -| blocklistManager | `string` | **No** | Admin | The blocklisted vault blocklist manager | -| metadataIpfsHash | `string` | **No** | Admin | The vault metadata IPFS hash | -| userAddress | `string` | **Yes** | - | The address of the user making the update (admin, whitelist manager, blocklist manager or keys manager) | -| vaultAddress | `string` | **Yes** | - | The address of the vault | +| Name | Type | Required | Access | Description | +|---------------------------|----------------------------------------------|----------|----------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| whitelistManager | `Array<{ address: string, isNew: boolean }>` | **No** | whitelistManager | List of addresses to update the whitelist. Use `isNew: true` to add a new address, `isNew: false` to remove an existing one. Max count at time - 700 addresses. | +| blocklist | `Array<{ address: string, isNew: boolean }>` | **No** | Blocklist manager | List of addresses to update the blocklist. Use `isNew: true` to add a new address, `isNew: false` to remove an existing one. Max count at time - 700 addresses. | +| depositDataManager | `string` | **No** | Deposit-data manager | Address of the vault keys manager. Support only **first version** on valults. For second verion use `vault.setDepositDataManager` | +| validatorsManager | `string` | **No** | Admin | Address of the vault deposit data manager. Support only **second version** on valults. | +| restakeWithdrawalsManager | `string` | **No** | Admin | The restake withdrawals manager must be assigned to the wallet connected to the operator service. It is responsible for withdrawing exiting validators from the EigenLayer. | +| restakeOperatorsManager | `string` | **No** | Admin | The restake operators manager can add EigenPods and update EigenLayer operators. | +| whitelistManager | `string` | **No** | Admin | Address of the vault whitelistManager | +| feeRecipient | `string` | **No** | Admin | Address of the vault fee recipient | +| validatorsRoot | `string` | **No** | Keys manager | The vault validators merkle tree root. Support only **first version** on valults. For second verion use `vault.setDepositDataRoot` | +| blocklistManager | `string` | **No** | Admin | The blocklisted vault blocklist manager | +| image | `string` | **No** | Admin | The vault image in base64 string format (will be uploaded to IPFS; maximum size is 1 MB) | +| displayName | `string` | **No** | Admin | The vault display name (will be uploaded to IPFS; maximum size is 30 characters) | +| description | `string` | **No** | Admin | The vault description (will be uploaded to IPFS; maximum size is 1000 characters) | +| userAddress | `string` | **Yes** | - | The address of the user making the update (admin, whitelist manager, blocklist manager or keys manager) | +| vaultAddress | `string` | **Yes** | - | The address of the vault | #### Example: @@ -1457,8 +1512,10 @@ Updates the vault by authorized personnel such as the vault admin, whitelistMana const params = { userAddress: '0x...', vaultAddress: '0x...', + image: '...', + displayName: '...', + description: '...', feeRecipient: '0x...', - metadataIpfsHash: '...', validatorsRoot: '0x...', blocklistManager: '0x...', whitelistManager: '0x...', @@ -1466,7 +1523,6 @@ const params = { depositDataManager: '0x...', restakeOperatorsManager: '0x...', restakeWithdrawalsManager: '0x...', - } // Data to update the vault by vault keys manager. diff --git a/changelog/next-release.md b/changelog/next-release.md index 927d9f86..ce0b8e7e 100644 --- a/changelog/next-release.md +++ b/changelog/next-release.md @@ -1,574 +1,73 @@ -- Update **eslint** -> 8.56.0 -- Update **node** version -> 20.12.2 -- Update Backend GraphQ **url** -> "https://holesky-api.stakewise.io/graphql" -- Add removeOldFiles helper. The removeOldFiles function deletes all files with the extension .graphql.ts in subdirectories of the specified directory, if there is no corresponding file with the extension .graphql. -If the directory does not contain any .graphql files, it is deleted along with all its contents. -- Refactor **multicall** contracts. Implement new **commonMulticall**, **vaultMulticall**, **eigenPodOwnerMulticall**, **rewardSplitterMulticall**. -- Implement new logic for **encode** & **estimateGas** to all transaction methods -- Add new check access utils methods: `checkAdminAccess | checkBlocklistManagerAccess | checkDepositDataManagerAccess | checkRestakeOperatorsManagerAccess | checkWhitelisterAccess`, to more thoroughly verify access for vault transactions +- Add the sdk.vault.create method +- Add a getVaultFactory helper to retrieve the vault factory contract based on the provided vault type and ERC20 token availability +- Update the sdk.vault.operate method to accept `image`, `displayName`, and `description` instead of `metadataIpfsHash` # Updates +### sdk.vault.operate -### sdk.vault.getExitQueuePositions - -#### Returns: -```ts -type Output = { - ...oldOutput, - duration: number | null -} -``` -| Name | Description | -|------|-----------------------| -| `duration` | Total queue duration time (in seconds).
- It represents the approximate time after which the assets can be collected (in seconds).
- If the value is null, the time is still being calculated.
- If the value is 0, the assets are available and can be collected. (New*) | ---- -### sdk.vault.getVault - -#### Returns: -```ts -type Output = { - ...oldOutput, - version: number - isRestake: boolean - whitelistManager: string - depositDataManager: string - restakeOperatorsManager: string - restakeWithdrawalsManager: string -} -``` - -| Name | Description | Status | -|--------------------|------------------|----------------| -| `whitelister` | - | **Deprecated!** | -| `vaultKeysManager` | - | **Deprecated!** | -| `version` | Vault version (1 or 2) | **New** | -| `isRestake` | Indicates whether the Vault is a restaking vault | **New** | -| `whitelistManager` | Whitelist | **New** | -| `depositDataManager` | Keys | **New** | -| `restakeOperatorsManager` | If the Vault is a restaking vault, restake operators manager can add/remove restake operators | **New** | -| `restakeWithdrawalsManager` | If the Vault is a restaking vault, restake withdrawals manager can manage EigenLayer withdrawals | **New** | ---- -### sdk.vault.getMaxWithdraw - -#### New arguments: -| Name | Type | Required | Info | -|------|------|-------------|-------| -| vaultAddress | `bigint` | **Yes** | Address of vault | - ---- -### sdk.vault.updateWhitelist **Deprecated!** -#### Description: -Use **sdk.vault.operate** instead - ---- -### sdk.vault.updateBlocklist **Deprecated!** -#### Description: -Use **sdk.vault.operate** instead - ---- ---- -## osToken -### sdk.osToken.getBurnAmount - -#### New arguments: -| Name | Type | Required | Info | -|------|------|-------------|-------| -| vaultAddress | `string` | **Yes** | Address of vault | +#### Removed arguments: ---- -### sdk.osToken.getMaxMint +| Name | Type | Required | Access | Description | +|------------------|----------|----------|--------|------------------------------| +| metadataIpfsHash | `string` | **No** | Admin | The vault metadata IPFS hash | #### New arguments: -| Name | Type | Required | Info | -|------|------|-------------|-------| -| vaultAddress | `string` | **Yes** | Address of vault | ---- -### sdk.osToken.getBaseData **Deprecated!** -#### Description: -Use **osToken.getConfig** and **osToken.getRate** +| Name | Type | Required | Access | Description | +|---------------------------|----------------------------------------------|----------|----------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| image | `string` | **No** | Admin | The vault image in base64 string format (will be uploaded to IPFS; maximum size is 1 MB) | +| displayName | `string` | **No** | Admin | The vault display name (will be uploaded to IPFS; maximum size is 30 characters) | +| description | `string` | **No** | Admin | The vault description (will be uploaded to IPFS; maximum size is 1000 characters) | ---- # New -## Vault -### sdk.vault.getRewardSplitters - -Fetch the list of created reward splitters. A reward splitter is a contract designed to distribute vault rewards among multiple fee recipients in predefined proportions. -To use a reward splitter, its address should be added to the vault as a fee recipient. - -#### Arguments: -| Name | Type | Type | Description | -|------|----------|---------|--------------------------------------------------------------------------------------------------------------------------------------------| -| vaultAddress | `string` | **Yes** | The address of the vault | -| owner | `string` | **Yes** | The owner of the reward splitter | -| rewardSplitterAddress | `string` | **No** | The address of the reward splitter (optional) | - -#### Returns: -```ts -type FeeRecipient = { - shares: bigint - percent: number - address: string -} - -type RewardSplitter = { - owner: string - address: string - totalShares: bigint - feeRecipients: FeeRecipient[] -} - -type Output = { - rewardSplitters: RewardSplitter[] -} -``` -| Name | Description | -|-------------------|-------------| -| `rewardSplitters` | An array of objects representing the result of the query based on your parameters | | ---- -### sdk.vault.getEigenPods - -Returns eigen pods for restake vault. - -#### Arguments: -| Name | Type | Required | Description | -|------|------|----------|-------------| -| vaultAddress | `string` | **Yes** | The address of the vault | -| limit | `number` | **No** | Limits the number of eigen pods returned. Defaults to 100 | -| skip | `number` | **No** | Skips the specified number of eigen pods. Defaults to 0 | - -#### Returns: -```ts -type Output = { - link: string - owner: string - operator: string - restaked: string - createdAt: number - podAddress: string -} -``` -| Name | Description | -|-------------|---------------------------------| -| `createdAt` | Date of Creation | -| `link` | Link to beaconchain | -| `operator` | The eigenPod's operator | -| `podAddress` | The eigenPod's address | -| `restaked` | EgenPod's restaked (in ETH) | -| `owner` | The address of the eigen pod owner | ---- - -### sdk.vault.setDepositDataRoot - -#### Description: - -Adding root validators to vaults **version 2** or higher - -#### Arguments: - -| Name | Type | Required | Description | -|----------------|----------|----------|-------------| -| validatorsRoot | `string` | **Yes** | The vault validators merkle tree | -| userAddress | `string` | **Yes** | - | -| vaultAddress | `string` | **Yes** | - | - -#### Example: - -```ts -const params = { - validatorsRoot: 'hash', - vaultAddress: '0x...', - userAddress: '0x...', -} - -// Send transaction -const hash = await sdk.vault.setDepositDataRoot(params) -// When you sign transactions on the backend (for custodians) -const { data, to } = await sdk.vault.setDepositDataRoot.encode(params) -// Get an approximate gas per transaction -const gas = await sdk.vault.setDepositDataRoot.estimateGas(params) -``` ---- -### sdk.vault.setDepositDataManager - -#### Description: - -Adding deposit data manager to vaults **version 2** or higher - -#### Arguments: - -| Name | Type | Required | Description | -|----------------|----------|----------|-------------| -| managerAddress | `string` | **Yes** | New deposit-data manager | -| userAddress | `string` | **Yes** | - | -| vaultAddress | `string` | **Yes** | - | - -#### Example: - -```ts -const params = { - managerAddress: '0x...', - vaultAddress: '0x...', - userAddress: '0x...', -} - -// Send transaction -const hash = await sdk.vault.setDepositDataManager(params) -// When you sign transactions on the backend (for custodians) -const { data, to } = await sdk.vault.setDepositDataManager.encode(params) -// Get an approximate gas per transaction -const gas = await sdk.vault.setDepositDataManager.estimateGas(params) -``` ---- -### sdk.vault.createEigenPod - -#### Description: - -Adding eigen pod to the vault. Only for restake vaults and only restake operators manager can perform this action. - -#### Arguments: - -| Name | Type | Required | Description | -|----------------|----------|----------|-----------| -| userAddress | `string` | **Yes** | The address of the user making the request | -| vaultAddress | `string` | **Yes** | The address of the vault | - -#### Example: - -```ts -const params = { - vaultAddress: '0x...', - userAddress: '0x...', -} - -// Send transaction -const hash = await sdk.vault.createEigenPod(params) -// When you sign transactions on the backend (for custodians) -const { data, to } = await sdk.vault.createEigenPod.encode(params) -// Get an approximate gas per transaction -const gas = await sdk.vault.createEigenPod.estimateGas(params) -``` ---- -### sdk.vault.setEigenPodOperator - -#### Description: - -Adding operator to the current eigen pod. This action is specific to restake vaults and can only be executed by the restake operators manager. - -#### Arguments: - -| Name | Type | Required | Description | -|----------------|----------|----------|----------------------------------| -| userAddress | `string` | **Yes** | The address of the user making the request | -| vaultAddress | `string` | **Yes** | The address of the vault | -| ownerAddress | `string` | **Yes** | The address of the eigen pod owner | -| operatorAddress | `string` | **Yes** | New operator for current eigen pods | - -#### Example: - -```ts -const params = { - operatorAddress: '0x...', -} - -// Send transaction -const hash = await sdk.vault.setEigenPodOperator(params) -// When you sign transactions on the backend (for custodians) -const { data, to } = await sdk.vault.setEigenPodOperator.encode(params) -// Get an approximate gas per transaction -const gas = await sdk.vault.setEigenPodOperator.estimateGas(params) -``` ---- -### sdk.vault.updateEigenPodOperator - -#### Description: - -Update operator to the current eigen pod. This action is specific to restake vaults and can only be executed by the restake operators manager. - -#### Arguments: - -| Name | Type | Required | Description | -|----------------|----------|----------|----------------------------------| -| userAddress | `string` | **Yes** | The address of the user making the request | -| vaultAddress | `string` | **Yes** | The address of the vault | -| ownerAddress | `string` | **Yes** | The address of the eigen pod owner | -| operatorAddress | `string` | **Yes** | New operator for current eigen pods | - -#### Example: - -```ts -const params = { - operatorAddress: '0x...', -} - -// Send transaction -const hash = await sdk.vault.updateEigenPodOperator(params) -// When you sign transactions on the backend (for custodians) -const { data, to } = await sdk.vault.updateEigenPodOperator.encode(params) -// Get an approximate gas per transaction -const gas = await sdk.vault.updateEigenPodOperator.estimateGas(params) -``` ---- -### sdk.vault.operate +### `sdk.vault.create` #### Description: -Updates the vault by authorized personnel such as the vault admin, whitelistManager, blocklist manager, validators manager, or deposit-data manager. +Create a vault. When the transaction is executed, one gwei of the deposit token must be stored in the vault to avoid [inflation attack](https://blog.openzeppelin.com/a-novel-defense-against-erc4626-inflation-attacks). +Pay attention to chains where the deposit token is not a native token (such as Gnosis or Chiado). +On these chains before creating the vault, ensure that you call the `approve` function on the deposit token contract, +allowing the vault factory address to spend one gwei. +You can retrieve the vault factory contract using the helper function: `sdk.getVaultFactory({ vaultType: params.type, isErc20: params.isErc20 })`. #### Arguments: -| Name | Type | Required | Access | Description | -|------------------|----------------------------------------------|----------|-------------------|-----------------------------------------------------------------------------------------------------------------------------| -| whitelistManager | `Array<{ address: string, isNew: boolean }>` | **No** | whitelistManager | List of addresses to update the whitelist. Use `isNew: true` to add a new address, `isNew: false` to remove an existing one. Max count at time - 700 addresses. | -| blocklist | `Array<{ address: string, isNew: boolean }>` | **No** | Blocklist manager | List of addresses to update the blocklist. Use `isNew: true` to add a new address, `isNew: false` to remove an existing one. Max count at time - 700 addresses. | -| depositDataManager | `string` | **No** | Deposit-data manager | Address of the vault keys manager. Support only **first version** on valults. For second verion use `vault.setDepositDataManager` | -| validatorsManager | `string` | **No** | Admin | Address of the vault deposit data manager. Support only **second version** on valults. | -| restakeWithdrawalsManager | `string` | **No** | Admin | The restake withdrawals manager must be assigned to the wallet connected to the operator service. It is responsible for withdrawing exiting validators from the EigenLayer. | -| restakeOperatorsManager | `string` | **No** | Admin | The restake operators manager can add EigenPods and update EigenLayer operators. | -| whitelistManager | `string` | **No** | Admin | Address of the vault whitelistManager | -| feeRecipient | `string` | **No** | Admin | Address of the vault fee recipient | -| validatorsRoot | `string` | **No** | Keys manager | The vault validators merkle tree root. Support only **first version** on valults. For second verion use `vault.setDepositDataRoot` | -| blocklistManager | `string` | **No** | Admin | The blocklisted vault blocklist manager | -| metadataIpfsHash | `string` | **No** | Admin | The vault metadata IPFS hash | -| userAddress | `string` | **Yes** | - | The address of the user making the update (admin, whitelist manager, blocklist manager or keys manager) | -| vaultAddress | `string` | **Yes** | - | The address of the vault | +| Name | Type | Required | Description | +|----------------|------------------------------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------------------| +| userAddress | `string` | **Yes** | The address of the user initiating the action. This address will become the vault admin | +| type | `VaultType` | **No** | Allowed vault types: Default, Private and Blocklist. Available vault types can be found in the `enum VaultType` which you can be imported from the library | +| vaultToken | `{ name: string, symbol: string }` | **No** | If provided, the vault will be created with its own ERC20 token | +| capacity | `bigint` | **No** | If provided, should be defined in gwei. By default, capacity is `MaxUint256`; the minimum allowed capacity is `parseEther('32')` | +| keysManagerFee | `number` | **No** | If provided, should be between `0` and `100`, inclusive with a maximum of two decimal digits allowed (e.g., `15.35`). By default, the fee is `0` | +| isOwnMevEscrow | `boolean` | **No** | Defines whether to send block rewards to the Smoothing Pool (`false`) or keep them only to your Vault (`true`). By default, this value is `false` | +| image | `string` | **No** | The vault image in base64 string format (will be uploaded to IPFS; maximum size is 1 MB) | +| displayName | `string` | **No** | The vault display name (will be uploaded to IPFS; maximum size is 30 characters) | +| description | `string` | **No** | The vault description (will be uploaded to IPFS; maximum size is 1000 characters) | #### Example: ```ts -// Data to update the vault by admin. -const params = { - userAddress: '0x...', - vaultAddress: '0x...', - feeRecipient: '0x...', - metadataIpfsHash: '...', - validatorsRoot: '0x...', - blocklistManager: '0x...', - whitelistManager: '0x...', - validatorsManager: '0x...', - depositDataManager: '0x...', - restakeOperatorsManager: '0x...', - restakeWithdrawalsManager: '0x...', - -} - -// Data to update the vault by vault keys manager. const params = { - validatorsRoot: '...', - vaultAddress: '0x...', userAddress: '0x...', -} - -// Data to update the private vault by whitelist manager. -// The whitelist contains addresses allowed to stake or mint within -// the vault. -const params = { - whitelist: [ - { - address: '0x...', - isNew: true, - }, - { - address: '0x...', - isNew: false, - }, - ], - vaultAddress: '0x...', - userAddress: '0x...', -} - -// Data to update blocklisted vault by blocklist manager. -// The blocklist contains addresses disallowed to stake or mint within -// the vault. -const params = { - blocklist: [ - { - address: '0x...', - isNew: true, - }, - { - address: '0x...', - isNew: false, - }, - ], - vaultAddress: '0x...', - userAddress: '0x...', -} - -// Send transaction -const hash = await sdk.vault.operate(params) -// When you sign transactions on the backend (for custodians) -const { data, to } = await sdk.vault.operate.encode(params) -// Get an approximate gas per transaction -const gas = await sdk.vault.operate.estimateGas(params) -``` - ---- -### sdk.osToken.getRate - -Returns ETH - osToken rate - -#### Returns: -```ts -type Output = string -``` ---- -### sdk.osToken.getConfig - -#### Description: -Basic information on the token - -#### Arguments: -| Name | Type | Required | Description | -|--------------|----------|----------|---------------| -| vaultAddress | `string` | **Yes** | Vault address | -#### Returns: -```ts -type Output = { - ltvPercent: bigint - thresholdPercent: bigint -} -``` -| Name | Description | -|------|-------------| -| `ltvPercent` | The percent used to calculate how much user can mint OsToken shares | -| `thresholdPercent` | The liquidation threshold percent used to calculate health factor for OsToken position | ---- -## RewardSplitter -### sdk.rewardSplitter.getClaimAmount - -#### Description: -Calculates the amount of assets that the user can claim from the reward splitter. - -#### Arguments: -| Name | Type | Required | Description | -|-----------------------|----------|----------|--------------------------------------------| -| userAddress | `string` | **Yes** | The address of the user making the request | -| vaultAddress | `string` | **Yes** | The address of the vault | -| rewardSplitterAddress | `string` | **Yes** | The address of the reward splitter | - -#### Returns: -```ts -type Output = bigint -``` ---- -### sdk.rewardSplitter.create - -#### Description: -Creates a reward splitter contract to distribute vault rewards among multiple fee recipients in predefined proportions. -Subsequently, the address of the created reward splitter must be added to the vault as a fee recipient in order to -utilize it. Please note that only vault admin is permitted to perform this action. - -#### Arguments: -| Name | Type | Required | Description | -|--------------|----------|----------|--------------------------------------------------------------------------------------------------------------------------------------| -| userAddress | `string` | **Yes** | The address of the user initiating the action. This address will become the owner of the reward splitter and must be the vault admin | -| vaultAddress | `string` | **Yes** | The address of the vault | - -#### Example: - -```ts -const params = { - vaultAddress: '0x...', - userAddress: '0x...', -} - -// Send transaction -const hash = await sdk.rewardSplitter.create(params) -// When you sign transactions on the backend (for custodians) -const { data, to } = await sdk.rewardSplitter.create.encode(params) -// Get an approximate gas per transaction -const gas = await sdk.rewardSplitter.create.estimateGas(params) -``` ---- -### sdk.rewardSplitter.claimRewards - -#### Description: - -Claims rewards from the reward splitter contract - -#### Arguments: -| Name | Type | Required | Description | -|-----------------------|----------|----------|--------------------------------------------------------------------------------------------------------------------------------------| -| userAddress | `string` | **Yes** | The address of the user initiating the action. This address will become the owner of the reward splitter and must be the vault admin | -| vaultAddress | `string` | **Yes** | The address of the vault | -| rewardSplitterAddress | `string` | **Yes** | The address of the reward splitter | -| assets | `bigint` | **Yes** | The amount of assets to claim | - -#### Example: - -```ts -const params = { - vaultAddress: '0x...', - userAddress: '0x...', - rewardSplitterAddress: '0x...', - assets: parseEther('100'), -} - -// Send transaction -const hash = await sdk.rewardSplitter.claimRewards(params) -// When you sign transactions on the backend (for custodians) -const { data, to } = await sdk.rewardSplitter.claimRewards.encode(params) -// Get an approximate gas per transaction -const gas = await sdk.rewardSplitter.claimRewards.estimateGas(params) -``` ---- -### sdk.rewardSplitter.updateFeeRecipients - -#### Description: - -Updates the reward splitter fee recipients and predefined fee splitting proportions. -Please note that only the vault admin, who is also the owner of the reward splitter, is permitted to perform this action. - -#### Arguments: - -| Name | Type | Required | Description | -|-----------------------|----------------------------------------------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| userAddress | `string` | **Yes** | The address of the user initiating the action. It should be the vault admin, who is also the owner of the reward splitter. | -| vaultAddress | `string` | **Yes** | The address of the vault | -| rewardSplitterAddress | `string` | **Yes** | The address of the reward splitter | -| feeRecipients | `Array<{ address: string, shares: bigint }>` | **Yes** | The list of the vault fee recipients with their addresses and shares amount. For simplicity, we suggest setting the amount as a percentage converted to a BigInt value. For example, for 100% shares: `parseEther('100')` | -| oldFeeRecipients | `Array<{ address: string, shares: bigint }>` | **No** | The current list of the vault fee recipients that will be updated within this action. It is needed to calculate how many shares will be added or removed from each fee recipient. If not provided, it will be requested from the [sdk.vault.getRewardSplitters](#sdkvaultgetrewardsplitters) action | - -#### Example: - -```ts -const params = { - vaultAddress: '0x...', - userAddress: '0x...', - rewardSplitterAddress: '0x...', - feeRecipients: [ - { - address: '0x...1', // The fee for this address will be increased from 20% to 50%. - shares: parseEther('50'), - }, - { - address: '0x...4', // This address will be added as a fee recipient with 50% fee distribution. - shares: parseEther('50'), - }, - ], - oldFeeRecipients: [ - { - address: '0x...1', // The fee for this address will be increased from 20% to 50%. - shares: parseEther('20'), - }, - { - address: '0x...2', // This address will be removed from the fee recipients since it is not in the `feeRecipients` list. - shares: parseEther('40'), - }, - { - address: '0x...3', // This address will also be removed from the fee recipients. - shares: parseEther('40'), - }, - ], -} - -// Send transaction -const hash = await sdk.rewardSplitter.updateFeeRecipients(params) + type: VaultType.Default, + vaultToken: { + name: 'Vault Token', + symbol: 'vlt', + }, + capacity: MaxUint256, + keysManagerFee: 0, + isOwnMevEscrow: false, + image: 'data:image/png;base64,...', + displayName: 'Example vault', + description: 'Example description', +} + +// Transaction example +// Send transaction to create a vault +const hash = await sdk.vault.create(params) // When you sign transactions on the backend (for custodians) -const { data, to } = await sdk.rewardSplitter.updateFeeRecipients.encode(params) +const { data, to, value } = await sdk.vault.deposit.encode(params) // Get an approximate gas per transaction -const gas = await sdk.rewardSplitter.updateFeeRecipients.estimateGas(params) -``` ---- +const gas = await sdk.vault.deposit.estimateGas(params) +``` \ No newline at end of file diff --git a/package.json b/package.json index a19101cb..642a1df5 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "1.3.1", + "version": "1.3.2", "sideEffects": false, "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/scripts/generateGraphqlExports/mutationTemplate.ts b/scripts/generateGraphqlExports/mutationTemplate.ts index 4ef33bc9..5905107d 100644 --- a/scripts/generateGraphqlExports/mutationTemplate.ts +++ b/scripts/generateGraphqlExports/mutationTemplate.ts @@ -7,14 +7,34 @@ type SubmitInput = { variables: {QueryName}Variables } +type Output = { + data: {QueryName}Payload + errors?: { + path: string[] + message: string + extensions: { + code: string + } + }[] +} + const submit{QueryName} = ({ variables, url }: SubmitInput) => fetch(url, { method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, body: JSON.stringify({ query, variables, }) }) - .then((res) => res.json() as Promise<{ data: {QueryName}Payload }>) - .then(({ data }) => data) + .then((res) => res.json() as Promise) + .then(({ data, errors }) => { + if (!errors) { + return data + } + + return Promise.reject(errors) + }) ` diff --git a/src/StakeWiseSDK.ts b/src/StakeWiseSDK.ts index 335c3127..94f90da2 100644 --- a/src/StakeWiseSDK.ts +++ b/src/StakeWiseSDK.ts @@ -1,8 +1,9 @@ import methods from './methods' -import { configs, getGas, createProvider } from './utils' +import { configs, getGas, createProvider, getVaultFactory, VaultType } from './utils' import { createContracts, vaultMulticall, rewardSplitterMulticall } from './contracts' +type GetVaultFactoryInput = { vaultType?: VaultType, isErc20?: boolean } type VaultMulticallInput = Pick[0], 'request' | 'userAddress' | 'vaultAddress'> type RewardSplitterMulticallInput = Pick[0], 'request' | 'userAddress' | 'vaultAddress'> & { rewardSplitterAddress: string @@ -38,6 +39,14 @@ class StakeWiseSDK { this.rewardSplitter = methods.createRewardSplitterMethods(argsForMethods) } + getVaultFactory({ vaultType, isErc20 }: GetVaultFactoryInput) { + return getVaultFactory({ + vaultType, + isErc20, + contracts: this.contracts, + }) + } + vaultMulticall({ userAddress, vaultAddress, request }: VaultMulticallInput) { return vaultMulticall({ vaultContract: this.contracts.helpers.createVault(vaultAddress), diff --git a/src/contracts/multicall/util/handleTransactionData.ts b/src/contracts/multicall/util/handleTransactionData.ts index 193b6279..0ee4b66b 100644 --- a/src/contracts/multicall/util/handleTransactionData.ts +++ b/src/contracts/multicall/util/handleTransactionData.ts @@ -10,12 +10,7 @@ type Input = { const handleTransactionData = async ({ contract, multicallParams }: Input) => { const calls = getMulticallCalls({ multicallParams, contract }) - const rx = await contract.multicall.populateTransaction(calls) - - return { - to: rx.to, - data: rx.data, - } + return contract.multicall.populateTransaction(calls) } diff --git a/src/graphql/backend/vault/index.ts b/src/graphql/backend/vault/index.ts index 00ce3b17..e589a5c8 100644 --- a/src/graphql/backend/vault/index.ts +++ b/src/graphql/backend/vault/index.ts @@ -9,3 +9,6 @@ export type { UserRewardsQueryPayload, UserRewardsQueryVariables } from './userR export { fetchScorePercentilesQuery } from './scorePercentilesQuery.graphql' export type { ScorePercentilesQueryPayload, ScorePercentilesQueryVariables } from './scorePercentilesQuery.graphql' + +export { submitUploadMetadataMutation } from './uploadMetadataMutation.graphql' +export type { UploadMetadataMutationPayload, UploadMetadataMutationVariables } from './uploadMetadataMutation.graphql' diff --git a/src/graphql/backend/vault/uploadMetadataMutation.graphql b/src/graphql/backend/vault/uploadMetadataMutation.graphql new file mode 100644 index 00000000..a752997a --- /dev/null +++ b/src/graphql/backend/vault/uploadMetadataMutation.graphql @@ -0,0 +1,9 @@ +mutation UploadMetadata ( + $payload: PayloadType! +) { + uploadMetadata( + payload: $payload + ) { + ipfsHash + } +} diff --git a/src/methods/osToken/transactions/burn/burnEncode.ts b/src/methods/osToken/transactions/burn/burnEncode.ts index cf6b1ba1..9801344b 100644 --- a/src/methods/osToken/transactions/burn/burnEncode.ts +++ b/src/methods/osToken/transactions/burn/burnEncode.ts @@ -7,12 +7,7 @@ const burnEncode = async (values: BurnInput): Promise const vaultContract = commonLogic(values) - const rx = await vaultContract.burnOsToken.populateTransaction(shares) - - return { - data: rx.data, - to: rx.to, - } + return vaultContract.burnOsToken.populateTransaction(shares) } diff --git a/src/methods/rewardSplitter/transactions/claimRewards/claimRewardsEncode.ts b/src/methods/rewardSplitter/transactions/claimRewards/claimRewardsEncode.ts index 9eff3a04..477a1079 100644 --- a/src/methods/rewardSplitter/transactions/claimRewards/claimRewardsEncode.ts +++ b/src/methods/rewardSplitter/transactions/claimRewards/claimRewardsEncode.ts @@ -6,18 +6,13 @@ import { rewardSplitterMulticall } from '../../../../contracts' const claimRewardsEncode = async (values: ClaimRewardsInput) => { const multicallArgs = await commonLogic(values) - const rx = await rewardSplitterMulticall<{ data: string, to: string }>({ + return rewardSplitterMulticall<{ data: string, to: string }>({ ...multicallArgs, request: { ...multicallArgs.request, transactionData: true, }, }) - - return { - data: rx.data, - to: rx.to, - } } diff --git a/src/methods/rewardSplitter/transactions/createRewardSplitter/createRewardSplitterEncode.ts b/src/methods/rewardSplitter/transactions/createRewardSplitter/createRewardSplitterEncode.ts index 6476c5ad..07c7044a 100644 --- a/src/methods/rewardSplitter/transactions/createRewardSplitter/createRewardSplitterEncode.ts +++ b/src/methods/rewardSplitter/transactions/createRewardSplitter/createRewardSplitterEncode.ts @@ -5,12 +5,7 @@ import type { CreateRewardSplitterInput } from './types' const createRewardSplitterEncode = async (values: CreateRewardSplitterInput) => { const rewardSplitterFactory = await commonLogic(values) - const rx = await rewardSplitterFactory.createRewardSplitter.populateTransaction(values.vaultAddress) - - return { - data: rx.data, - to: rx.to, - } + return rewardSplitterFactory.createRewardSplitter.populateTransaction(values.vaultAddress) } diff --git a/src/methods/rewardSplitter/transactions/updateFeeRecipients/updateFeeRecipientsEncode.ts b/src/methods/rewardSplitter/transactions/updateFeeRecipients/updateFeeRecipientsEncode.ts index f68e5b2f..fd4dfca4 100644 --- a/src/methods/rewardSplitter/transactions/updateFeeRecipients/updateFeeRecipientsEncode.ts +++ b/src/methods/rewardSplitter/transactions/updateFeeRecipients/updateFeeRecipientsEncode.ts @@ -6,18 +6,13 @@ import { rewardSplitterMulticall } from '../../../../contracts' const updateFeeRecipientsEncode = async (values: UpdateFeeRecipientsInput) => { const multicallArgs = await commonLogic(values) - const rx = await rewardSplitterMulticall<{ data: string, to: string }>({ + return rewardSplitterMulticall<{ data: string, to: string }>({ ...multicallArgs, request: { ...multicallArgs.request, transactionData: true, }, }) - - return { - data: rx.data, - to: rx.to, - } } diff --git a/src/methods/utils/getVaultMulticallEncode.ts b/src/methods/utils/getVaultMulticallEncode.ts index f2bbb32d..94cd2251 100644 --- a/src/methods/utils/getVaultMulticallEncode.ts +++ b/src/methods/utils/getVaultMulticallEncode.ts @@ -4,18 +4,13 @@ import { vaultMulticall } from '../../contracts' type Input = Parameters[0] const getVaultMulticallEncode = async (values: Input): Promise => { - const rx = await vaultMulticall<{ data: string, to: string }>({ + return vaultMulticall<{ data: string, to: string }>({ ...values, request: { ...values.request, transactionData: true, }, }) - - return { - data: rx.data, - to: rx.to, - } } diff --git a/src/methods/vault/index.ts b/src/methods/vault/index.ts index 630eae7e..bedc4d42 100644 --- a/src/methods/vault/index.ts +++ b/src/methods/vault/index.ts @@ -15,6 +15,7 @@ import getScorePercentiles from './requests/getScorePercentiles' import getExitQueuePositions from './requests/getExitQueuePositions' // Transactions +import { default as create } from './transactions/create' import { default as deposit } from './transactions/deposit' import { default as operate } from './transactions/operate' import { default as withdraw } from './transactions/withdraw' @@ -131,6 +132,11 @@ export default { * @see https://github.com/stakewise/v3-sdk/?tab=readme-ov-file#sdkvaultoperate */ operate, + /** + * @description Create vault + * @see https://github.com/stakewise/v3-sdk/?tab=readme-ov-file#sdkvaultcreate + */ + create, /** * @description Adding root validators to vault * @throws Supports only the second version of vault diff --git a/src/methods/vault/transactions/create/common.ts b/src/methods/vault/transactions/create/common.ts new file mode 100644 index 00000000..2f2f3d04 --- /dev/null +++ b/src/methods/vault/transactions/create/common.ts @@ -0,0 +1,70 @@ +import { AbiCoder, MaxUint256 } from 'ethers' +import type { CreateVaultTransactionInput } from './types' +import { PayableOverrides } from '../../../../contracts/types/common' +import { constants, Network, validateArgs, VaultType } from '../../../../utils' +import validateCreateVaultArgs from './validateCreateVaultArgs' + + +export const commonLogic = async (values: CreateVaultTransactionInput) => { + const { + type = VaultType.Default, vaultToken, capacity, keysManagerFee, isOwnMevEscrow = false, metadataIpfsHash, + contracts, userAddress, options, + } = values + + validateArgs.address({ userAddress }) + validateCreateVaultArgs.vaultType(type) + validateCreateVaultArgs.mevEscrow(isOwnMevEscrow) + + if (vaultToken) { + validateCreateVaultArgs.vaultToken(vaultToken) + } + + if (capacity) { + validateArgs.bigint({ capacity }) + validateCreateVaultArgs.capacity(capacity) + } + + if (keysManagerFee) { + validateCreateVaultArgs.keysManagerFee(keysManagerFee) + } + + const vaultFactories = vaultToken ? { + [VaultType.Default]: contracts.factories.erc20Vault, + [VaultType.Private]: contracts.factories.erc20PrivateVault, + [VaultType.Blocklist]: contracts.factories.erc20BlocklistVault, + } : { + [VaultType.Default]: contracts.factories.vault, + [VaultType.Private]: contracts.factories.privateVault, + [VaultType.Blocklist]: contracts.factories.blocklistVault, + } + + const vaultFactory = vaultFactories[type] + const defaultAbiCoder = AbiCoder.defaultAbiCoder() + + const formattedParams = { + feePercent: (keysManagerFee || 0) * 100, + capacity: capacity || MaxUint256, + symbol: vaultToken?.symbol, + name: vaultToken?.name, + } + + const encodedParams = vaultToken + ? defaultAbiCoder.encode( + [ 'tuple(uint256 capacity, uint16 feePercent, string name, string symbol, string metadataIpfsHash)' ], + [ [ formattedParams.capacity, formattedParams.feePercent, formattedParams.name, formattedParams.symbol, metadataIpfsHash ] ] + ) + : defaultAbiCoder.encode( + [ 'tuple(uint256 capacity, uint16 feePercent, string metadataIpfsHash)' ], + [ [ formattedParams.capacity, formattedParams.feePercent, metadataIpfsHash ] ] + ) + + const isStakeNativeToken = [ Network.Mainnet, Network.Holesky ].includes(options.network) + const params: [ string, boolean, PayableOverrides ] = isStakeNativeToken + ? [ encodedParams, isOwnMevEscrow, { value: constants.blockchain.gwei } ] + : [ encodedParams, isOwnMevEscrow, {} ] + + return { + vaultFactory, + params, + } +} diff --git a/src/methods/vault/transactions/create/createVaultEncode.ts b/src/methods/vault/transactions/create/createVaultEncode.ts new file mode 100644 index 00000000..456eb4ae --- /dev/null +++ b/src/methods/vault/transactions/create/createVaultEncode.ts @@ -0,0 +1,22 @@ +import { commonLogic } from './common' +import type { CreateVaultInput } from './types' +import { uploadMetadata } from '../util' + + +const createVaultEncode = async (values: CreateVaultInput) => { + const { image, displayName, description, ...rest } = values + const { options } = rest + + let metadataIpfsHash = '' + + if (image || displayName || description) { + metadataIpfsHash = await uploadMetadata({ image, displayName, description, options }) + } + + const { vaultFactory, params } = await commonLogic({ metadataIpfsHash, ...rest }) + + return vaultFactory.createVault.populateTransaction(...params) +} + + +export default createVaultEncode diff --git a/src/methods/vault/transactions/create/createVaultGas.ts b/src/methods/vault/transactions/create/createVaultGas.ts new file mode 100644 index 00000000..af76383c --- /dev/null +++ b/src/methods/vault/transactions/create/createVaultGas.ts @@ -0,0 +1,23 @@ +import { commonLogic } from './common' +import { getGas } from '../../../../utils' +import type { CreateVaultInput } from './types' +import { getMetadataHashMock } from '../util' + + +const createVaultGas = async (values: CreateVaultInput) => { + const { image, displayName, description, ...rest } = values + const { provider, userAddress } = rest + + const metadataIpfsHash = getMetadataHashMock({ image, displayName, description }) + const { vaultFactory, params } = await commonLogic({ metadataIpfsHash, ...rest }) + + const signer = await provider.getSigner(userAddress) + const signedContract = vaultFactory.connect(signer) + + const estimatedGas = await signedContract.createVault.estimateGas(...params) + + return getGas({ estimatedGas, provider }) +} + + +export default createVaultGas diff --git a/src/methods/vault/transactions/create/index.ts b/src/methods/vault/transactions/create/index.ts new file mode 100644 index 00000000..3e8b2170 --- /dev/null +++ b/src/methods/vault/transactions/create/index.ts @@ -0,0 +1,32 @@ +import { commonLogic } from './common' +import createVaultGas from './createVaultGas' +import createVaultEncode from './createVaultEncode' +import { uploadMetadata } from '../util' +import type { CreateVault } from './types' + + +const create: CreateVault = async (values) => { + const { image, displayName, description, ...rest } = values + const { provider, options, userAddress } = rest + + let metadataIpfsHash = '' + + if (image || displayName || description) { + metadataIpfsHash = await uploadMetadata({ image, displayName, description, options }) + } + + const { vaultFactory, params } = await commonLogic({ metadataIpfsHash, ...rest }) + + const signer = await provider.getSigner(userAddress) + const signedContract = vaultFactory.connect(signer) + + const response = await signedContract.createVault(...params) + + return response.hash +} + +create.encode = createVaultEncode +create.estimateGas = createVaultGas + + +export default create diff --git a/src/methods/vault/transactions/create/types.d.ts b/src/methods/vault/transactions/create/types.d.ts new file mode 100644 index 00000000..489abd9e --- /dev/null +++ b/src/methods/vault/transactions/create/types.d.ts @@ -0,0 +1,29 @@ +import createVaultGas from './createVaultGas' +import createVaultEncode from './createVaultEncode' +import type { BaseInput } from '../../../utils' +import type { VaultType } from '../../../../utils' +import type { UploadMetadataInput } from '../util/metadata/uploadMetadata' + + +export type CreateVaultCommonInput = Omit & { + type?: VaultType + vaultToken?: { + name: string + symbol: string + } + capacity?: bigint + keysManagerFee?: number + isOwnMevEscrow?: boolean +} + +export type CreateVaultInput = CreateVaultCommonInput & UploadMetadataInput + +export type CreateVaultTransactionInput = CreateVaultCommonInput & { + metadataIpfsHash: string | null +} + +export interface CreateVault { + (values: CreateVaultInput): Promise + estimateGas: typeof createVaultGas + encode: typeof createVaultEncode +} diff --git a/src/methods/vault/transactions/create/validateCreateVaultArgs.ts b/src/methods/vault/transactions/create/validateCreateVaultArgs.ts new file mode 100644 index 00000000..5de07e72 --- /dev/null +++ b/src/methods/vault/transactions/create/validateCreateVaultArgs.ts @@ -0,0 +1,75 @@ +import { MaxUint256 } from 'ethers' +import { VaultType, constants } from '../../../../utils' + + +const capacity = (value: bigint) => { + if (value < constants.blockchain.amount32) { + throw new Error(`The "capacity" argument must be at least ${constants.blockchain.amount32}`) + } + if (value > MaxUint256) { + throw new Error(`The "capacity" argument must be at most ${MaxUint256}`) + } +} + +const mevEscrow = (value: unknown) => { + if (typeof value !== 'boolean') { + throw new Error(`The "isOwnMevEscrow" argument must be of boolean type`) + } +} + +const vaultType = (value: VaultType) => { + const vaultTypes = Object.values(VaultType) + const isValidType = vaultTypes.includes(value) + + if (!isValidType) { + throw new Error(`The "type" argument must be one of the following: ${vaultTypes.join(', ')}`) + } +} + +const vaultToken = (vaultToken: { name: string, symbol: string }) => { + if (typeof vaultToken !== 'object') { + throw new Error(`The "vaultToken" argument must be an object`) + } + + const missingParams = Object.keys(vaultToken) + .filter((key) => typeof vaultToken[key as keyof typeof vaultToken] !== 'string') + + if (missingParams.length) { + const args = missingParams.map((key) => `"vaultToken.${key}"`).join(', ') + + throw new Error(`The ${args} ${missingParams.length === 1 ? 'argument' : 'arguments'} must be a string`) + } + + const emptyParams = Object.keys(vaultToken) + .filter((key) => !vaultToken[key as keyof typeof vaultToken]) + + if (emptyParams.length) { + const args = emptyParams.map((key) => `"vaultToken.${key}"`).join(', ') + + throw new Error(`The ${args} ${missingParams.length === 1 ? 'argument' : 'arguments'} must be not empty string`) + } +} + +const keysManagerFee = (value: number) => { + if (value < 0) { + throw new Error(`The "keysManagerFee" argument must be at least 0`) + } + if (value > 100) { + throw new Error(`The "keysManagerFee" argument must be at most 100`) + } + + const decimals = value.toString().split('.')[1]?.length + + if (decimals > 2) { + throw new Error(`The "keysManagerFee" argument must have at most two decimal places`) + } +} + + +export default { + capacity, + mevEscrow, + vaultType, + vaultToken, + keysManagerFee, +} diff --git a/src/methods/vault/transactions/createEigenPod/createEigenPodEncode.ts b/src/methods/vault/transactions/createEigenPod/createEigenPodEncode.ts index 5b66bbf2..39a441f8 100644 --- a/src/methods/vault/transactions/createEigenPod/createEigenPodEncode.ts +++ b/src/methods/vault/transactions/createEigenPod/createEigenPodEncode.ts @@ -5,12 +5,7 @@ import { CreateEigenPodInput } from './types' const depositEncode = async (values: CreateEigenPodInput) => { const contract = await commonLogic(values) - const rx = await contract.createEigenPod.populateTransaction() - - return { - to: rx.to, - data: rx.data, - } + return contract.createEigenPod.populateTransaction() } diff --git a/src/methods/vault/transactions/deposit/nativeToken/depositEncode.ts b/src/methods/vault/transactions/deposit/nativeToken/depositEncode.ts index 3a221ccc..5c4ea72a 100644 --- a/src/methods/vault/transactions/deposit/nativeToken/depositEncode.ts +++ b/src/methods/vault/transactions/deposit/nativeToken/depositEncode.ts @@ -18,8 +18,7 @@ const depositEncode = async (values: DepositInput): Promise = const rx = await vaultContract.updateStateAndDeposit.populateTransaction(userAddress, referrer, harvestParams, overrides) return { - to: rx.to, - data: rx.data, + ...rx, value: overrides.value, } } @@ -27,8 +26,7 @@ const depositEncode = async (values: DepositInput): Promise = const rx = await vaultContract.deposit.populateTransaction(userAddress, referrer, overrides) return { - to: rx.to, - data: rx.data, + ...rx, value: overrides.value, } } diff --git a/src/methods/vault/transactions/operate/checkAccess.ts b/src/methods/vault/transactions/operate/checkAccess.ts index debce8ff..e0cdcbac 100644 --- a/src/methods/vault/transactions/operate/checkAccess.ts +++ b/src/methods/vault/transactions/operate/checkAccess.ts @@ -1,4 +1,4 @@ -import type { MulticallInput as Input } from './types' +import type { MulticallTransactionInput as Input } from './types' import { checkAdminAccess, diff --git a/src/methods/vault/transactions/operate/common.ts b/src/methods/vault/transactions/operate/common.ts index c5ff9041..3c08e247 100644 --- a/src/methods/vault/transactions/operate/common.ts +++ b/src/methods/vault/transactions/operate/common.ts @@ -1,4 +1,4 @@ -import type { MulticallInput } from './types' +import type { MulticallTransactionInput } from './types' import { validateArgs } from '../../../../utils' import { vaultMulticall } from '../../../../contracts' @@ -17,7 +17,7 @@ import { } from '../util' -export const commonLogic = async (values: MulticallInput) => { +export const commonLogic = async (values: MulticallTransactionInput) => { const { validatorsRoot, blocklistManager, metadataIpfsHash, blocklist, whitelist, depositDataManager, whitelistManager, feeRecipient, @@ -116,7 +116,7 @@ export const commonLogic = async (values: MulticallInput) => { params.push(...validatorsRootParams) } - if (metadataIpfsHash) { + if (typeof metadataIpfsHash !== 'undefined') { const metadataParams = getMetadataParams({ ...baseInput, metadataIpfsHash }) params.push(...metadataParams) diff --git a/src/methods/vault/transactions/operate/index.ts b/src/methods/vault/transactions/operate/index.ts index 1fecbeb6..ff3ca6a3 100644 --- a/src/methods/vault/transactions/operate/index.ts +++ b/src/methods/vault/transactions/operate/index.ts @@ -4,10 +4,15 @@ import type { Multicall } from './types' import multicallGas from './multicallGas' import multicallEncode from './multicallEncode' import { vaultMulticall } from '../../../../contracts' +import { uploadMetadata } from '../util' const operate: Multicall = async (values) => { - const multicallCommonArgs = await commonLogic(values) + const { image, displayName, description, ...rest } = values + const { options } = rest + + const metadataIpfsHash = await uploadMetadata({ image, displayName, description, options }) + const multicallCommonArgs = await commonLogic({ metadataIpfsHash, ...rest }) const result = await vaultMulticall<{ hash: string }>(multicallCommonArgs) diff --git a/src/methods/vault/transactions/operate/multicallEncode.ts b/src/methods/vault/transactions/operate/multicallEncode.ts index a7317e32..205c85f4 100644 --- a/src/methods/vault/transactions/operate/multicallEncode.ts +++ b/src/methods/vault/transactions/operate/multicallEncode.ts @@ -1,10 +1,15 @@ -import { getVaultMulticallEncode } from '../../../utils' -import type { MulticallInput } from './types' import { commonLogic } from './common' +import type { MulticallInput } from './types' +import { uploadMetadata } from '../util' +import { getVaultMulticallEncode } from '../../../utils' const multicallEncode = async (values: MulticallInput): Promise => { - const multicallArgs = await commonLogic(values) + const { image, displayName, description, ...rest } = values + const { options } = rest + + const metadataIpfsHash = await uploadMetadata({ image, displayName, description, options }) + const multicallArgs = await commonLogic({ metadataIpfsHash, ...rest }) return getVaultMulticallEncode(multicallArgs) } diff --git a/src/methods/vault/transactions/operate/multicallGas.ts b/src/methods/vault/transactions/operate/multicallGas.ts index d5ced89f..e5999f22 100644 --- a/src/methods/vault/transactions/operate/multicallGas.ts +++ b/src/methods/vault/transactions/operate/multicallGas.ts @@ -1,12 +1,15 @@ import { commonLogic } from './common' import { getVaultMulticallGas } from '../../../utils' import type { MulticallInput } from './types' +import { getMetadataHashMock } from '../util' const multicallGas = async (props: MulticallInput) => { - const { provider } = props + const { image, displayName, description, ...rest } = props + const { provider } = rest - const multicallArgs = await commonLogic(props) + const metadataIpfsHash = getMetadataHashMock({ image, displayName, description }) + const multicallArgs = await commonLogic({ metadataIpfsHash, ...rest }) return getVaultMulticallGas({ ...multicallArgs, provider }) } diff --git a/src/methods/vault/transactions/operate/types.d.ts b/src/methods/vault/transactions/operate/types.d.ts index febd4948..f5ff00be 100644 --- a/src/methods/vault/transactions/operate/types.d.ts +++ b/src/methods/vault/transactions/operate/types.d.ts @@ -1,6 +1,7 @@ import multicallGas from './multicallGas' import multicallEncode from './multicallEncode' +import type { UploadMetadataInput } from '../util/metadata/uploadMetadata' import type { SetMetadataParams } from '../util/params/getMetadataParams' import type { UpdateBlocklistParams } from '../util/params/getBlocklistParams' import type { UpdateWhitelistParams } from '../util/params/getWhitelistParams' @@ -21,9 +22,8 @@ type BaseInput = { provider: StakeWise.Provider } -type MulticallParams = Partial< - SetMetadataParams - & SetWhitelisterParams +type MulticallCommonParams = + SetWhitelisterParams & SetFeeRecipientParams & UpdateBlocklistParams & UpdateWhitelistParams @@ -33,8 +33,19 @@ type MulticallParams = Partial< & SetDepositDataManagerParams & SetRestakeOperatorsManagerParams & SetRestakeWithdrawalsManagerParams + +type MulticallParams = Partial< + UploadMetadataInput + & MulticallCommonParams > +type MulticallTransactionParams = Partial< + SetMetadataParams + & MulticallCommonParams +> + +export type MulticallTransactionInput = BaseInput & MulticallTransactionParams + export type MulticallInput = BaseInput & MulticallParams export interface Multicall { diff --git a/src/methods/vault/transactions/setDepositDataManager/setDepositDataManagerEncode.ts b/src/methods/vault/transactions/setDepositDataManager/setDepositDataManagerEncode.ts index 4e7e2e21..73292292 100644 --- a/src/methods/vault/transactions/setDepositDataManager/setDepositDataManagerEncode.ts +++ b/src/methods/vault/transactions/setDepositDataManager/setDepositDataManagerEncode.ts @@ -5,12 +5,7 @@ import type { SetDepositDataManagerInput } from './types' const setDepositDataManagerEncode = async (values: SetDepositDataManagerInput) => { const contract = commonLogic(values) - const rx = await contract.setDepositDataManager.populateTransaction(values.vaultAddress, values.managerAddress) - - return { - to: rx.to, - data: rx.data, - } + return contract.setDepositDataManager.populateTransaction(values.vaultAddress, values.managerAddress) } diff --git a/src/methods/vault/transactions/setDepositDataRoot/setDepositDataRootEncode.ts b/src/methods/vault/transactions/setDepositDataRoot/setDepositDataRootEncode.ts index c5ce956a..1afccf18 100644 --- a/src/methods/vault/transactions/setDepositDataRoot/setDepositDataRootEncode.ts +++ b/src/methods/vault/transactions/setDepositDataRoot/setDepositDataRootEncode.ts @@ -5,12 +5,7 @@ import type { SetDepositDataRootInput } from './types' const setDepositDataRootEncode = async (values: SetDepositDataRootInput) => { const contract = commonLogic(values) - const rx = await contract.setDepositDataRoot.populateTransaction(values.vaultAddress, values.validatorsRoot) - - return { - to: rx.to, - data: rx.data, - } + return contract.setDepositDataRoot.populateTransaction(values.vaultAddress, values.validatorsRoot) } diff --git a/src/methods/vault/transactions/setEigenPodOperator/setEigenPodOperatorEncode.ts b/src/methods/vault/transactions/setEigenPodOperator/setEigenPodOperatorEncode.ts index 2079f479..483cfe00 100644 --- a/src/methods/vault/transactions/setEigenPodOperator/setEigenPodOperatorEncode.ts +++ b/src/methods/vault/transactions/setEigenPodOperator/setEigenPodOperatorEncode.ts @@ -5,12 +5,7 @@ import { SetEigenPodOperatorInput } from './types' const setEigenPodOperatorEncode = async (values: SetEigenPodOperatorInput) => { const { eigenPodOwnerContract } = await commonLogic(values) - const rx = await eigenPodOwnerContract.eigenPod.populateTransaction() - - return { - to: rx.to, - data: rx.data, - } + return eigenPodOwnerContract.eigenPod.populateTransaction() } diff --git a/src/methods/vault/transactions/updateEigenPodOperator/updateEigenPodOperatorEncode.ts b/src/methods/vault/transactions/updateEigenPodOperator/updateEigenPodOperatorEncode.ts index 4ad4cc9c..5399d820 100644 --- a/src/methods/vault/transactions/updateEigenPodOperator/updateEigenPodOperatorEncode.ts +++ b/src/methods/vault/transactions/updateEigenPodOperator/updateEigenPodOperatorEncode.ts @@ -6,18 +6,13 @@ import { eigenPodOwnerMulticall } from '../../../../contracts' const updateEigenPodOperatorEncode = async (values: UpdateEigenPodOperatorInput) => { const multicallArgs = await commonLogic(values) - const rx = await eigenPodOwnerMulticall<{ data: string, to: string }>({ + return eigenPodOwnerMulticall<{ data: string, to: string }>({ ...multicallArgs, request: { ...multicallArgs.request, transactionData: true, }, }) - - return { - data: rx.data, - to: rx.to, - } } diff --git a/src/methods/vault/transactions/util/index.ts b/src/methods/vault/transactions/util/index.ts index 97daeeb3..0c21970a 100644 --- a/src/methods/vault/transactions/util/index.ts +++ b/src/methods/vault/transactions/util/index.ts @@ -1,2 +1,3 @@ export * from './check' export * from './params' +export * from './metadata' diff --git a/src/methods/vault/transactions/util/metadata/getMetadataHashMock/index.ts b/src/methods/vault/transactions/util/metadata/getMetadataHashMock/index.ts new file mode 100644 index 00000000..9fe9eabb --- /dev/null +++ b/src/methods/vault/transactions/util/metadata/getMetadataHashMock/index.ts @@ -0,0 +1,17 @@ +type Input = { + image?: string + displayName?: string + description?: string +} + + +const getMetadataHashMock = ({ image, displayName, description }: Input) => { + if (image || displayName || description) { + return '00000000000000000000000000000000000000000000000000000000000' + } + + return '' +} + + +export default getMetadataHashMock diff --git a/src/methods/vault/transactions/util/metadata/index.ts b/src/methods/vault/transactions/util/metadata/index.ts new file mode 100644 index 00000000..5be77835 --- /dev/null +++ b/src/methods/vault/transactions/util/metadata/index.ts @@ -0,0 +1,2 @@ +export { default as uploadMetadata } from './uploadMetadata' +export { default as getMetadataHashMock } from './getMetadataHashMock' diff --git a/src/methods/vault/transactions/util/metadata/uploadMetadata/index.ts b/src/methods/vault/transactions/util/metadata/uploadMetadata/index.ts new file mode 100644 index 00000000..89ba4456 --- /dev/null +++ b/src/methods/vault/transactions/util/metadata/uploadMetadata/index.ts @@ -0,0 +1,51 @@ +import graphql from '../../../../../../graphql' +import { apiUrls, validateArgs } from '../../../../../../utils' + + +export type UploadMetadataInput = { + image?: string + displayName?: string + description?: string +} + +type Input = UploadMetadataInput & { + options: StakeWise.Options +} + +const uploadMetadata = async (input: Input) => { + const { image = '', displayName = '', description = '', options } = input + + if (image) { + validateArgs.image(image) + } + + validateArgs.maxLength({ + displayName: { value: displayName, length: 30 }, + description: { value: description, length: 1000 }, + }) + + validateArgs.string({ image, displayName, description }) + + try { + const data = await graphql.backend.vault.submitUploadMetadataMutation({ + url: apiUrls.getBackendUrl(options), + variables: { + payload: { + image, + displayName, + description, + }, + }, + }) + + return data?.uploadMetadata?.ipfsHash + } + catch (error) { + const errorCode = Array.isArray(error) ? error[0]?.extensions?.code : null + + return Promise.reject(errorCode || error) + } +} + + +export default uploadMetadata diff --git a/src/methods/vault/transactions/util/params/getMetadataParams.ts b/src/methods/vault/transactions/util/params/getMetadataParams.ts index 64914de8..2a3d2476 100644 --- a/src/methods/vault/transactions/util/params/getMetadataParams.ts +++ b/src/methods/vault/transactions/util/params/getMetadataParams.ts @@ -1,16 +1,14 @@ -import { validateArgs } from '../../../../../utils' import { vaultMulticall } from '../../../../../contracts' -export type SetMetadataParams = { - metadataIpfsHash: string +type SetMetadataParams = { + metadataIpfsHash: string | null } const getMetadataParams = (values: SetMetadataParams) => { const { metadataIpfsHash } = values - validateArgs.string({ metadataIpfsHash }) - + // ATTN we don't validate `metadataIpfsHash` because it is not provided by user const params: Parameters[0]['request']['params'] = [ { method: 'setMetadata', args: [ metadataIpfsHash ], diff --git a/src/utils/constants/blockchain.ts b/src/utils/constants/blockchain.ts index 2c9486b5..e6fd45ae 100644 --- a/src/utils/constants/blockchain.ts +++ b/src/utils/constants/blockchain.ts @@ -3,6 +3,7 @@ export default { secondsInBlock: 13, blocksInYear: 2427458, secondsInYear: 31536000, + gwei: 1000000000n, amount0: 0n, amount1: 1000000000000000000n, amount10: 10000000000000000000n, diff --git a/src/utils/enums.ts b/src/utils/enums.ts index beaee609..1e920aef 100644 --- a/src/utils/enums.ts +++ b/src/utils/enums.ts @@ -6,6 +6,12 @@ export enum Network { Chiado = 10200, } +export enum VaultType { + Default = 'Default', + Private = 'Private', + Blocklist = 'Blocklist', +} + export enum AllocatorActionType { Redeemed = 'Redeemed', Migrated = 'Migrated', diff --git a/src/utils/getVaultFactory.ts b/src/utils/getVaultFactory.ts new file mode 100644 index 00000000..1c3b7b54 --- /dev/null +++ b/src/utils/getVaultFactory.ts @@ -0,0 +1,25 @@ +import { VaultType } from './enums' + + +type Input = { + contracts: StakeWise.Contracts + vaultType?: VaultType + isErc20?: boolean +} + +const getVaultFactory = ({ vaultType, contracts, isErc20 }: Input) => { + const vaultFactories = isErc20 ? { + [VaultType.Default]: contracts.factories.erc20Vault, + [VaultType.Private]: contracts.factories.erc20PrivateVault, + [VaultType.Blocklist]: contracts.factories.erc20BlocklistVault, + } : { + [VaultType.Default]: contracts.factories.vault, + [VaultType.Private]: contracts.factories.privateVault, + [VaultType.Blocklist]: contracts.factories.blocklistVault, + } + + return vaultFactories[vaultType || VaultType.Default] +} + + +export default getVaultFactory diff --git a/src/utils/index.ts b/src/utils/index.ts index 41a9f10d..28644fab 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -8,4 +8,5 @@ export { default as BigDecimal } from './BigDecimal' export { default as validateArgs } from './validateArgs' export { default as createProvider } from './createProvider' export { default as getNetworkTypes } from './getNetworkTypes' +export { default as getVaultFactory } from './getVaultFactory' export { default as getValidLtvPercent } from './getValidLtvPercent' diff --git a/src/utils/validateArgs.ts b/src/utils/validateArgs.ts index 71a264a8..e3d946b7 100644 --- a/src/utils/validateArgs.ts +++ b/src/utils/validateArgs.ts @@ -1,6 +1,17 @@ import { isAddress } from 'ethers' +const image = (value: string) => { + const decode = value?.substring(value.indexOf(',') + 1) + const sizeMB = decode ? atob(decode).length / 1000 / 1000 : 0 + + if (sizeMB > 1) { + const currentSize = sizeMB.toFixed(2) + + throw new Error(`The size of the image must be under 1 MB, current size is ${currentSize} MB`) + } +} + const bigint = (values: Record) => { Object.keys(values).forEach((key) => { if (typeof values[key] !== 'bigint') { @@ -44,11 +55,22 @@ const array = (values: Record, withEmptyCheck: boolean = true) => }) } +const maxLength = (values: Record) => { + Object.keys(values).forEach((key) => { + const { value, length } = values[key] + if (value.length > length) { + throw new Error(`The value for "${key}" argument must be less than ${length} characters`) + } + }) +} + export default { + image, array, bigint, string, number, address, + maxLength, }