diff --git a/package.json b/package.json index 62abc31f..16d386bb 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "@valora/http-handler": "^0.0.1", "@valora/logging": "^1.3.18", "bignumber.js": "^9.1.2", + "cids": "^1.1.9", "dotenv": "^16.4.7", "express": "^4.21.2", "got": "^11.8.6", @@ -50,6 +51,7 @@ "i18next-fs-backend": "^2.6.0", "i18next-http-middleware": "^3.7.0", "lru-cache": "^11.0.2", + "multihashes": "^4.0.3", "semver": "^7.6.3", "viem": "^2.21.54", "zod": "^3.23.8" diff --git a/src/apps/ubeswap/abis/farm-registry.ts b/src/apps/ubeswap/abis/farm-registry.ts index 4e8bb3ff..0e6162cd 100644 --- a/src/apps/ubeswap/abis/farm-registry.ts +++ b/src/apps/ubeswap/abis/farm-registry.ts @@ -24,3 +24,1041 @@ export const FarmInfoEventAbi = { name: 'FarmInfo', type: 'event', } as const + +export const FarmAbi = [ + { + inputs: [ + { + internalType: 'contract IUniswapV3Factory', + name: '_factory', + type: 'address', + }, + { + internalType: 'contract INonfungiblePositionManager', + name: '_nonfungiblePositionManager', + type: 'address', + }, + { + internalType: 'uint256', + name: '_maxIncentiveStartLeadTime', + type: 'uint256', + }, + { + internalType: 'uint256', + name: '_maxIncentivePeriodDuration', + type: 'uint256', + }, + { internalType: 'uint256', name: '_maxLockTime', type: 'uint256' }, + ], + stateMutability: 'nonpayable', + type: 'constructor', + }, + { inputs: [], name: 'AccessControlBadConfirmation', type: 'error' }, + { + inputs: [ + { internalType: 'address', name: 'account', type: 'address' }, + { internalType: 'bytes32', name: 'neededRole', type: 'bytes32' }, + ], + name: 'AccessControlUnauthorizedAccount', + type: 'error', + }, + { + inputs: [{ internalType: 'address', name: 'target', type: 'address' }], + name: 'AddressEmptyCode', + type: 'error', + }, + { + inputs: [{ internalType: 'address', name: 'account', type: 'address' }], + name: 'AddressInsufficientBalance', + type: 'error', + }, + { inputs: [], name: 'FailedInnerCall', type: 'error' }, + { + inputs: [{ internalType: 'address', name: 'token', type: 'address' }], + name: 'SafeERC20FailedOperation', + type: 'error', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'uint256', + name: 'tokenId', + type: 'uint256', + }, + { + indexed: true, + internalType: 'address', + name: 'oldOwner', + type: 'address', + }, + { + indexed: true, + internalType: 'address', + name: 'newOwner', + type: 'address', + }, + ], + name: 'DepositTransferred', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'bytes32', + name: 'incentiveId', + type: 'bytes32', + }, + { + indexed: false, + internalType: 'uint128', + name: 'refund', + type: 'uint128', + }, + ], + name: 'ExcessRewardsRefunded', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'bytes32', + name: 'incentiveId', + type: 'bytes32', + }, + { indexed: false, internalType: 'address', name: 'to', type: 'address' }, + { + indexed: false, + internalType: 'uint256', + name: 'reward', + type: 'uint256', + }, + ], + name: 'ExternalRewardCollected', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'owner', + type: 'address', + }, + { + indexed: true, + internalType: 'uint256', + name: 'tokenId', + type: 'uint256', + }, + { + indexed: false, + internalType: 'address', + name: 'recipient', + type: 'address', + }, + { + indexed: false, + internalType: 'uint128', + name: 'amount0Max', + type: 'uint128', + }, + { + indexed: false, + internalType: 'uint128', + name: 'amount1Max', + type: 'uint128', + }, + ], + name: 'FeeCollected', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'bytes32', + name: 'incentiveId', + type: 'bytes32', + }, + { + indexed: true, + internalType: 'contract IERC20', + name: 'rewardToken', + type: 'address', + }, + { + indexed: true, + internalType: 'contract IUniswapV3Pool', + name: 'pool', + type: 'address', + }, + { + indexed: false, + internalType: 'uint32', + name: 'startTime', + type: 'uint32', + }, + { + indexed: false, + internalType: 'uint32', + name: 'lockTime', + type: 'uint32', + }, + { + indexed: false, + internalType: 'int24', + name: 'minimumTickRange', + type: 'int24', + }, + { + indexed: false, + internalType: 'int24', + name: 'maxTickLower', + type: 'int24', + }, + { + indexed: false, + internalType: 'int24', + name: 'minTickLower', + type: 'int24', + }, + { + indexed: false, + internalType: 'int24', + name: 'maxTickUpper', + type: 'int24', + }, + { + indexed: false, + internalType: 'int24', + name: 'minTickUpper', + type: 'int24', + }, + ], + name: 'IncentiveCreated', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'bytes32', + name: 'incentiveId', + type: 'bytes32', + }, + { + indexed: false, + internalType: 'uint32', + name: 'newPeriodId', + type: 'uint32', + }, + { + indexed: false, + internalType: 'uint32', + name: 'duration', + type: 'uint32', + }, + { + indexed: false, + internalType: 'uint128', + name: 'reward', + type: 'uint128', + }, + ], + name: 'IncentiveExtended', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'bytes32', + name: 'incentiveId', + type: 'bytes32', + }, + { + indexed: false, + internalType: 'uint32', + name: 'timestamp', + type: 'uint32', + }, + { + indexed: false, + internalType: 'uint32', + name: 'newPeriodId', + type: 'uint32', + }, + { + indexed: false, + internalType: 'bytes32', + name: 'merkleRoot', + type: 'bytes32', + }, + { + indexed: false, + internalType: 'bytes32', + name: 'ipfsHash', + type: 'bytes32', + }, + { + indexed: false, + internalType: 'uint128', + name: 'distributedRewardsSinceLastUpdate', + type: 'uint128', + }, + { + indexed: false, + internalType: 'uint128', + name: 'activeTvlNative', + type: 'uint128', + }, + { + indexed: false, + internalType: 'uint128', + name: 'externalTvlNative', + type: 'uint128', + }, + ], + name: 'IncentiveUpdated', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'bytes32', + name: 'incentiveId', + type: 'bytes32', + }, + { + indexed: false, + internalType: 'uint32', + name: 'periodId', + type: 'uint32', + }, + { + indexed: false, + internalType: 'uint128', + name: 'reward', + type: 'uint128', + }, + ], + name: 'PeriodRewardIncreased', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'uint256', + name: 'tokenId', + type: 'uint256', + }, + { + indexed: true, + internalType: 'bytes32', + name: 'incentiveId', + type: 'bytes32', + }, + { indexed: true, internalType: 'address', name: 'to', type: 'address' }, + { + indexed: false, + internalType: 'uint256', + name: 'reward', + type: 'uint256', + }, + ], + name: 'RewardCollected', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'bytes32', name: 'role', type: 'bytes32' }, + { + indexed: true, + internalType: 'bytes32', + name: 'previousAdminRole', + type: 'bytes32', + }, + { + indexed: true, + internalType: 'bytes32', + name: 'newAdminRole', + type: 'bytes32', + }, + ], + name: 'RoleAdminChanged', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'bytes32', name: 'role', type: 'bytes32' }, + { + indexed: true, + internalType: 'address', + name: 'account', + type: 'address', + }, + { + indexed: true, + internalType: 'address', + name: 'sender', + type: 'address', + }, + ], + name: 'RoleGranted', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'bytes32', name: 'role', type: 'bytes32' }, + { + indexed: true, + internalType: 'address', + name: 'account', + type: 'address', + }, + { + indexed: true, + internalType: 'address', + name: 'sender', + type: 'address', + }, + ], + name: 'RoleRevoked', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'uint256', + name: 'tokenId', + type: 'uint256', + }, + { + indexed: true, + internalType: 'bytes32', + name: 'incentiveId', + type: 'bytes32', + }, + { + indexed: false, + internalType: 'uint128', + name: 'liquidity', + type: 'uint128', + }, + { + indexed: false, + internalType: 'uint32', + name: 'initialSecondsInside', + type: 'uint32', + }, + ], + name: 'TokenStaked', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'uint256', + name: 'tokenId', + type: 'uint256', + }, + { + indexed: true, + internalType: 'bytes32', + name: 'incentiveId', + type: 'bytes32', + }, + ], + name: 'TokenUnstaked', + type: 'event', + }, + { + inputs: [], + name: 'DEFAULT_ADMIN_ROLE', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'INCENTIVE_MANAGER_ROLE', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'INCENTIVE_UPDATER_ROLE', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'REWARD_DISTRIBUTOR_ROLE', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + components: [ + { + internalType: 'contract IERC20', + name: 'rewardToken', + type: 'address', + }, + { + internalType: 'contract IUniswapV3Pool', + name: 'pool', + type: 'address', + }, + { internalType: 'uint32', name: 'startTime', type: 'uint32' }, + { internalType: 'uint32', name: 'lockTime', type: 'uint32' }, + { internalType: 'int24', name: 'minimumTickRange', type: 'int24' }, + { internalType: 'int24', name: 'maxTickLower', type: 'int24' }, + { internalType: 'int24', name: 'minTickLower', type: 'int24' }, + { internalType: 'int24', name: 'maxTickUpper', type: 'int24' }, + { internalType: 'int24', name: 'minTickUpper', type: 'int24' }, + ], + internalType: 'struct IUbeswapV3Farming.IncentiveKey', + name: 'key', + type: 'tuple', + }, + ], + name: 'collectExternalReward', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + components: [ + { internalType: 'uint256', name: 'tokenId', type: 'uint256' }, + { internalType: 'address', name: 'recipient', type: 'address' }, + { internalType: 'uint128', name: 'amount0Max', type: 'uint128' }, + { internalType: 'uint128', name: 'amount1Max', type: 'uint128' }, + ], + internalType: 'struct INonfungiblePositionManager.CollectParams', + name: 'params', + type: 'tuple', + }, + ], + name: 'collectFee', + outputs: [ + { internalType: 'uint256', name: 'amount0', type: 'uint256' }, + { internalType: 'uint256', name: 'amount1', type: 'uint256' }, + ], + stateMutability: 'payable', + type: 'function', + }, + { + inputs: [ + { + components: [ + { + internalType: 'contract IERC20', + name: 'rewardToken', + type: 'address', + }, + { + internalType: 'contract IUniswapV3Pool', + name: 'pool', + type: 'address', + }, + { internalType: 'uint32', name: 'startTime', type: 'uint32' }, + { internalType: 'uint32', name: 'lockTime', type: 'uint32' }, + { internalType: 'int24', name: 'minimumTickRange', type: 'int24' }, + { internalType: 'int24', name: 'maxTickLower', type: 'int24' }, + { internalType: 'int24', name: 'minTickLower', type: 'int24' }, + { internalType: 'int24', name: 'maxTickUpper', type: 'int24' }, + { internalType: 'int24', name: 'minTickUpper', type: 'int24' }, + ], + internalType: 'struct IUbeswapV3Farming.IncentiveKey', + name: 'key', + type: 'tuple', + }, + { internalType: 'uint256', name: 'tokenId', type: 'uint256' }, + { internalType: 'uint128', name: 'accumulatedRewards', type: 'uint128' }, + { internalType: 'bytes32[]', name: 'proof', type: 'bytes32[]' }, + ], + name: 'collectReward', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + components: [ + { + internalType: 'contract IERC20', + name: 'rewardToken', + type: 'address', + }, + { + internalType: 'contract IUniswapV3Pool', + name: 'pool', + type: 'address', + }, + { internalType: 'uint32', name: 'startTime', type: 'uint32' }, + { internalType: 'uint32', name: 'lockTime', type: 'uint32' }, + { internalType: 'int24', name: 'minimumTickRange', type: 'int24' }, + { internalType: 'int24', name: 'maxTickLower', type: 'int24' }, + { internalType: 'int24', name: 'minTickLower', type: 'int24' }, + { internalType: 'int24', name: 'maxTickUpper', type: 'int24' }, + { internalType: 'int24', name: 'minTickUpper', type: 'int24' }, + ], + internalType: 'struct IUbeswapV3Farming.IncentiveKey', + name: 'key', + type: 'tuple', + }, + { internalType: 'uint32', name: 'duration', type: 'uint32' }, + { internalType: 'uint128', name: 'reward', type: 'uint128' }, + ], + name: 'createIncentive', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + name: 'deposits', + outputs: [ + { internalType: 'address', name: 'owner', type: 'address' }, + { internalType: 'uint48', name: 'numberOfStakes', type: 'uint48' }, + { internalType: 'int24', name: 'tickLower', type: 'int24' }, + { internalType: 'int24', name: 'tickUpper', type: 'int24' }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + components: [ + { + internalType: 'contract IERC20', + name: 'rewardToken', + type: 'address', + }, + { + internalType: 'contract IUniswapV3Pool', + name: 'pool', + type: 'address', + }, + { internalType: 'uint32', name: 'startTime', type: 'uint32' }, + { internalType: 'uint32', name: 'lockTime', type: 'uint32' }, + { internalType: 'int24', name: 'minimumTickRange', type: 'int24' }, + { internalType: 'int24', name: 'maxTickLower', type: 'int24' }, + { internalType: 'int24', name: 'minTickLower', type: 'int24' }, + { internalType: 'int24', name: 'maxTickUpper', type: 'int24' }, + { internalType: 'int24', name: 'minTickUpper', type: 'int24' }, + ], + internalType: 'struct IUbeswapV3Farming.IncentiveKey', + name: 'key', + type: 'tuple', + }, + { internalType: 'uint32', name: 'newPeriodId', type: 'uint32' }, + { internalType: 'uint32', name: 'duration', type: 'uint32' }, + { internalType: 'uint128', name: 'reward', type: 'uint128' }, + ], + name: 'extendIncentive', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'externalRewardDistributor', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'factory', + outputs: [ + { internalType: 'contract IUniswapV3Factory', name: '', type: 'address' }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes32', name: 'incentiveId', type: 'bytes32' }, + { internalType: 'uint32', name: 'timestamp', type: 'uint32' }, + ], + name: 'getAccumulatedReward', + outputs: [ + { internalType: 'uint128', name: 'accumulatedReward', type: 'uint128' }, + { internalType: 'uint32', name: 'lastPeriodId', type: 'uint32' }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'bytes32', name: 'role', type: 'bytes32' }], + name: 'getRoleAdmin', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes32', name: 'incentiveId', type: 'bytes32' }, + { internalType: 'uint256', name: 'index', type: 'uint256' }, + ], + name: 'getStakedTokenByIndex', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes32', name: 'role', type: 'bytes32' }, + { internalType: 'address', name: 'account', type: 'address' }, + ], + name: 'grantRole', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes32', name: 'role', type: 'bytes32' }, + { internalType: 'address', name: 'account', type: 'address' }, + ], + name: 'hasRole', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes32', name: '', type: 'bytes32' }, + { internalType: 'uint32', name: '', type: 'uint32' }, + ], + name: 'incentivePeriods', + outputs: [ + { internalType: 'uint128', name: 'rewardPerSecond', type: 'uint128' }, + { internalType: 'uint32', name: 'startTime', type: 'uint32' }, + { internalType: 'uint32', name: 'endTime', type: 'uint32' }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + name: 'incentives', + outputs: [ + { internalType: 'uint32', name: 'currentPeriodId', type: 'uint32' }, + { internalType: 'uint32', name: 'lastUpdateTime', type: 'uint32' }, + { internalType: 'uint32', name: 'endTime', type: 'uint32' }, + { internalType: 'uint32', name: 'numberOfStakes', type: 'uint32' }, + { internalType: 'uint128', name: 'distributedRewards', type: 'uint128' }, + { internalType: 'bytes32', name: 'merkleRoot', type: 'bytes32' }, + { internalType: 'bytes32', name: 'ipfsHash', type: 'bytes32' }, + { internalType: 'uint128', name: 'excessRewards', type: 'uint128' }, + { internalType: 'uint128', name: 'externalRewards', type: 'uint128' }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + components: [ + { + internalType: 'contract IERC20', + name: 'rewardToken', + type: 'address', + }, + { + internalType: 'contract IUniswapV3Pool', + name: 'pool', + type: 'address', + }, + { internalType: 'uint32', name: 'startTime', type: 'uint32' }, + { internalType: 'uint32', name: 'lockTime', type: 'uint32' }, + { internalType: 'int24', name: 'minimumTickRange', type: 'int24' }, + { internalType: 'int24', name: 'maxTickLower', type: 'int24' }, + { internalType: 'int24', name: 'minTickLower', type: 'int24' }, + { internalType: 'int24', name: 'maxTickUpper', type: 'int24' }, + { internalType: 'int24', name: 'minTickUpper', type: 'int24' }, + ], + internalType: 'struct IUbeswapV3Farming.IncentiveKey', + name: 'key', + type: 'tuple', + }, + { internalType: 'uint32', name: 'periodId', type: 'uint32' }, + { internalType: 'uint128', name: 'reward', type: 'uint128' }, + ], + name: 'increasePeriodReward', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'maxIncentivePeriodDuration', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'maxIncentiveStartLeadTime', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'maxLockTime', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'bytes[]', name: 'data', type: 'bytes[]' }], + name: 'multicall', + outputs: [{ internalType: 'bytes[]', name: 'results', type: 'bytes[]' }], + stateMutability: 'payable', + type: 'function', + }, + { + inputs: [], + name: 'nonfungiblePositionManager', + outputs: [ + { + internalType: 'contract INonfungiblePositionManager', + name: '', + type: 'address', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: '', type: 'address' }, + { internalType: 'address', name: 'from', type: 'address' }, + { internalType: 'uint256', name: 'tokenId', type: 'uint256' }, + { internalType: 'bytes', name: 'data', type: 'bytes' }, + ], + name: 'onERC721Received', + outputs: [{ internalType: 'bytes4', name: '', type: 'bytes4' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + components: [ + { + internalType: 'contract IERC20', + name: 'rewardToken', + type: 'address', + }, + { + internalType: 'contract IUniswapV3Pool', + name: 'pool', + type: 'address', + }, + { internalType: 'uint32', name: 'startTime', type: 'uint32' }, + { internalType: 'uint32', name: 'lockTime', type: 'uint32' }, + { internalType: 'int24', name: 'minimumTickRange', type: 'int24' }, + { internalType: 'int24', name: 'maxTickLower', type: 'int24' }, + { internalType: 'int24', name: 'minTickLower', type: 'int24' }, + { internalType: 'int24', name: 'maxTickUpper', type: 'int24' }, + { internalType: 'int24', name: 'minTickUpper', type: 'int24' }, + ], + internalType: 'struct IUbeswapV3Farming.IncentiveKey', + name: 'key', + type: 'tuple', + }, + ], + name: 'refundExcessRewards', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes32', name: 'role', type: 'bytes32' }, + { internalType: 'address', name: 'callerConfirmation', type: 'address' }, + ], + name: 'renounceRole', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes32', name: 'role', type: 'bytes32' }, + { internalType: 'address', name: 'account', type: 'address' }, + ], + name: 'revokeRole', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + components: [ + { + internalType: 'contract IERC20', + name: 'rewardToken', + type: 'address', + }, + { + internalType: 'contract IUniswapV3Pool', + name: 'pool', + type: 'address', + }, + { internalType: 'uint32', name: 'startTime', type: 'uint32' }, + { internalType: 'uint32', name: 'lockTime', type: 'uint32' }, + { internalType: 'int24', name: 'minimumTickRange', type: 'int24' }, + { internalType: 'int24', name: 'maxTickLower', type: 'int24' }, + { internalType: 'int24', name: 'minTickLower', type: 'int24' }, + { internalType: 'int24', name: 'maxTickUpper', type: 'int24' }, + { internalType: 'int24', name: 'minTickUpper', type: 'int24' }, + ], + internalType: 'struct IUbeswapV3Farming.IncentiveKey', + name: 'key', + type: 'tuple', + }, + { internalType: 'uint256', name: 'tokenId', type: 'uint256' }, + ], + name: 'stakeToken', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes32', name: '', type: 'bytes32' }, + { internalType: 'uint256', name: '', type: 'uint256' }, + ], + name: 'stakes', + outputs: [ + { internalType: 'uint128', name: 'claimedReward', type: 'uint128' }, + { internalType: 'uint32', name: 'stakeTime', type: 'uint32' }, + { internalType: 'uint32', name: 'initialSecondsInside', type: 'uint32' }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'bytes4', name: 'interfaceId', type: 'bytes4' }], + name: 'supportsInterface', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'uint256', name: 'tokenId', type: 'uint256' }, + { internalType: 'address', name: 'to', type: 'address' }, + ], + name: 'transferDeposit', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + components: [ + { + internalType: 'contract IERC20', + name: 'rewardToken', + type: 'address', + }, + { + internalType: 'contract IUniswapV3Pool', + name: 'pool', + type: 'address', + }, + { internalType: 'uint32', name: 'startTime', type: 'uint32' }, + { internalType: 'uint32', name: 'lockTime', type: 'uint32' }, + { internalType: 'int24', name: 'minimumTickRange', type: 'int24' }, + { internalType: 'int24', name: 'maxTickLower', type: 'int24' }, + { internalType: 'int24', name: 'minTickLower', type: 'int24' }, + { internalType: 'int24', name: 'maxTickUpper', type: 'int24' }, + { internalType: 'int24', name: 'minTickUpper', type: 'int24' }, + ], + internalType: 'struct IUbeswapV3Farming.IncentiveKey', + name: 'key', + type: 'tuple', + }, + { internalType: 'uint256', name: 'tokenId', type: 'uint256' }, + ], + name: 'unstakeToken', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: '_new', type: 'address' }], + name: 'updateExternalRewardDistributor', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes32', name: 'incentiveId', type: 'bytes32' }, + { internalType: 'uint32', name: 'timestamp', type: 'uint32' }, + { internalType: 'bytes32', name: 'merkleRoot', type: 'bytes32' }, + { internalType: 'bytes32', name: 'ipfsHash', type: 'bytes32' }, + { + internalType: 'uint128', + name: 'distributedRewardsSinceLastUpdate', + type: 'uint128', + }, + { internalType: 'uint128', name: 'activeTvlNative', type: 'uint128' }, + { internalType: 'uint128', name: 'externalTvlNative', type: 'uint128' }, + ], + name: 'updateIncentiveDistributionInfo', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'uint256', name: 'tokenId', type: 'uint256' }, + { internalType: 'address', name: 'to', type: 'address' }, + { internalType: 'bytes', name: 'data', type: 'bytes' }, + ], + name: 'withdrawToken', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, +] diff --git a/src/apps/ubeswap/interface.ts b/src/apps/ubeswap/interface.ts new file mode 100644 index 00000000..8bde9bd7 --- /dev/null +++ b/src/apps/ubeswap/interface.ts @@ -0,0 +1,133 @@ +import { BigNumber } from '@ethersproject/bignumber' + +// From ubeswap-interface-v3/apps/web/src/pages/Earn/data/v3-incentive-list.ts, +// see: https://github.com/Ubeswap/ubeswap-interface-v3/blob/2357359112e42985a3f16c9c84055b12f7886d95/apps/web/src/pages/Earn/data/v3-incentive-list.ts#L4 +export interface IncentiveKey { + rewardToken: string + pool: string + startTime: number + lockTime: number + minimumTickRange: number + maxTickLower: number + minTickLower: number + maxTickUpper: number + minTickUpper: number +} + +export const incentiveIds: string[] = [ + '0xeec6459eb0d7379623c6b1d8b323cc64dea67f43e6ca85e8909a27424d21e812', + '0x3b85446788d259ca857dbb337cdb9ba3557a7fe0ab296ee405b8d2fd51d2500d', + '0x82774b5b1443759f20679a61497abf11115a4d0e2076caedf9d700a8c53f286f', + '0x114570896ebb76092b5bca76943aa8d7792fec67a65ac7b4c809cacfbb79fac0', + '0x2262434f83b2caa9bfcd1a3d0463b58001bfb235f08b3fd78d5815604a26f72d', +] + +const MIN_TICK = -887272 +const MAX_TICK = 887272 + +export const incentiveKeys: Record = { + '0xeec6459eb0d7379623c6b1d8b323cc64dea67f43e6ca85e8909a27424d21e812': { + rewardToken: '0x71e26d0e519d14591b9de9a0fe9513a398101490', + pool: '0x3efc8d831b754d3ed58a2b4c37818f2e69dadd19', + startTime: 1725104100, + lockTime: 0, + minimumTickRange: 0, + maxTickLower: MAX_TICK, + minTickLower: MIN_TICK, + maxTickUpper: MAX_TICK, + minTickUpper: MIN_TICK, + }, + '0x3b85446788d259ca857dbb337cdb9ba3557a7fe0ab296ee405b8d2fd51d2500d': { + rewardToken: '0x471ece3750da237f93b8e339c536989b8978a438', + pool: '0x3efc8d831b754d3ed58a2b4c37818f2e69dadd19', + startTime: 1725105600, + lockTime: 0, + minimumTickRange: 0, + maxTickLower: MAX_TICK, + minTickLower: MIN_TICK, + maxTickUpper: MAX_TICK, + minTickUpper: MIN_TICK, + }, + '0x82774b5b1443759f20679a61497abf11115a4d0e2076caedf9d700a8c53f286f': { + rewardToken: '0x71e26d0e519d14591b9de9a0fe9513a398101490', + pool: '0x28ade0134b9d0bc7041f4e5ea74fecb58504720b', + startTime: 1727713800, + lockTime: 0, + minimumTickRange: 0, + maxTickLower: MAX_TICK, + minTickLower: MIN_TICK, + maxTickUpper: MAX_TICK, + minTickUpper: MIN_TICK, + }, + '0x114570896ebb76092b5bca76943aa8d7792fec67a65ac7b4c809cacfbb79fac0': { + rewardToken: '0x471ece3750da237f93b8e339c536989b8978a438', + pool: '0x28ade0134b9d0bc7041f4e5ea74fecb58504720b', + startTime: 1727713800, + lockTime: 0, + minimumTickRange: 0, + maxTickLower: MAX_TICK, + minTickLower: MIN_TICK, + maxTickUpper: MAX_TICK, + minTickUpper: MIN_TICK, + }, + '0x2262434f83b2caa9bfcd1a3d0463b58001bfb235f08b3fd78d5815604a26f72d': { + rewardToken: '0x4f604735c1cf31399c6e711d5962b2b3e0225ad3', + pool: '0x28ade0134b9d0bc7041f4e5ea74fecb58504720b', + startTime: 1727713800, + lockTime: 0, + minimumTickRange: 0, + maxTickLower: MAX_TICK, + minTickLower: MIN_TICK, + maxTickUpper: MAX_TICK, + minTickUpper: MIN_TICK, + }, +} + +export const getIncentiveIdsByPool = (poolAddress: string) => { + return incentiveIds.filter((incentiveId) => { + return incentiveKeys[incentiveId].pool == poolAddress + }) +} + +export type StakeInfo = { + claimedReward: BigNumber + stakeTime: number + initialSecondsInside: number +} + +export interface IncentiveContractInfo { + currentPeriodId: number + lastUpdateTime: number + endTime: number + numberOfStakes: number + distributedRewards: BigNumber + merkleRoot: string + ipfsHash: string + excessRewards: BigNumber + externalRewards: BigNumber +} + +export interface IncentiveDataItem { + tokenId: BigNumber + accumulatedRewards: BigNumber + lastSecondsInsideOfTickRange: number + tvlNative: BigNumber + shares: BigNumber + duration: number + activeDuration: number + merkleProof: string[] | null + isStaked: boolean + isActive: boolean +} + +export interface TokenData { + tokenId: BigNumber + incentiveData: IncentiveDataItem | undefined + stakeInfo: + | { + claimedReward: BigNumber + stakeTime: number + initialSecondsInside: number + } + | undefined +} diff --git a/src/apps/ubeswap/positions.ts b/src/apps/ubeswap/positions.ts index 4df366ea..c90557a0 100644 --- a/src/apps/ubeswap/positions.ts +++ b/src/apps/ubeswap/positions.ts @@ -1,8 +1,7 @@ -import BigNumber from 'bignumber.js' +import BigNumberJs from 'bignumber.js' import got from '../../utils/got' import { Address, createPublicClient, http } from 'viem' import { celo } from 'viem/chains' -import { erc20Abi } from '../../abis/erc-20' import { getTokenId } from '../../runtime/getTokenId' import { NetworkId } from '../../types/networkId' import { DecimalNumber, toDecimalNumber } from '../../types/numbers' @@ -19,6 +18,22 @@ import { stakingRewardsAbi } from './abis/staking-rewards' import { uniswapV2PairAbi } from './abis/uniswap-v2-pair' import farms from './data/farms.json' import { logger } from '../../log' +import * as dotenv from 'dotenv' +import { BigNumber } from '@ethersproject/bignumber' +import CID from 'cids' +import { toB58String } from 'multihashes' +import { FarmAbi } from './abis/farm-registry' +import { + getIncentiveIdsByPool, + IncentiveContractInfo, + IncentiveDataItem, + StakeInfo, + TokenData, +} from './interface' +import { hexToUint8Array } from '../../utils/numbers' +import { erc20Abi } from '../../abis/erc-20' + +dotenv.config() const client = createPublicClient({ chain: celo, @@ -28,6 +43,8 @@ const client = createPublicClient({ const UBESWAP_LOGO = 'https://raw.githubusercontent.com/valora-inc/dapp-list/ab12ab234b4a6e01eff599c6bd0b7d5b44d6f39d/assets/ubeswap.png' +const IPFS_GATEWAY = 'https://gateway.pinata.cloud/ipfs/' + const PAIRS_QUERY = ` query getPairs($address: ID!) { user(id: $address) { @@ -41,6 +58,14 @@ const PAIRS_QUERY = ` } ` +const UBE_DECIMALS = 18 + +const FARM_POOLS = [ + // CELO/UBE pool + '0x3efc8d831b754d3ed58a2b4c37818f2e69dadd19', +] +const FARM_ADDRESS = '0xA6E9069CB055a425Eb41D185b740B22Ec8f51853' + async function getPoolPositionDefinition( networkId: NetworkId, poolAddress: Address, @@ -144,7 +169,7 @@ async function getPoolPositionDefinitions( // Get the pairs from Ubeswap via The Graph const response = await got .post( - 'https://gateway-arbitrum.network.thegraph.com/api/3f1b45f0fd92b4f414a3158b0381f482/subgraphs/id/JWDRLCwj4H945xEkbB6eocBSZcYnibqcJPJ8h9davFi', + `https://gateway-arbitrum.network.thegraph.com/api/${process.env.THE_GRAPH_API_KEY}/subgraphs/id/JWDRLCwj4H945xEkbB6eocBSZcYnibqcJPJ8h9davFi`, { json: { query: PAIRS_QUERY, @@ -177,6 +202,203 @@ async function getPoolPositionDefinitions( return positions } +async function fetchIncentiveFullData( + incentiveId: string, +): Promise { + const resp = (await client.readContract({ + address: FARM_ADDRESS, + abi: FarmAbi, + functionName: 'incentives', + args: [incentiveId], + })) as (number | string | BigNumber)[] + const incentiveInfo = { + currentPeriodId: resp[0], + lastUpdateTime: resp[1], + endTime: resp[2], + numberOfStakes: resp[3], + distributedRewards: resp[4], + merkleRoot: resp[5], + ipfsHash: resp[6], + excessRewards: resp[7], + externalRewards: resp[8], + } as IncentiveContractInfo + let ipfsHash = incentiveInfo?.ipfsHash + if (!ipfsHash) { + return + } + + let result: IncentiveDataItem[] = [] + if ( + ipfsHash != + '0x0000000000000000000000000000000000000000000000000000000000000000' + ) { + ipfsHash = ipfsHash.replace(/^0x/, '') + const cidV0 = new CID(toB58String(hexToUint8Array('1220' + ipfsHash))) + const cidV1Str = cidV0.toV1().toString() + + const data = await got(IPFS_GATEWAY + cidV1Str + '/data.json').json() + data.forEach((d: any) => { + d.tokenId = BigNumber.from(d.tokenId) + d.accumulatedRewards = BigNumber.from(d.accumulatedRewards) + d.tvlNative = BigNumber.from(d.tvlNative) + d.shares = BigNumber.from(d.shares) + }) + result = data as IncentiveDataItem[] + } + return result +} + +const fetchStakedTokenIds = async (address: string, incentiveIds: string[]) => { + const data = (await got('https://interface-gateway.ubeswap.org/v1/graphql', { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + operationName: 'FarmV3AccountStakes', + variables: { + address, + incentiveIds, + }, + query: '', + }), + }).json()) as { data: { stakes: { tokenId: string }[] } } + + return [ + ...new Set(data.data.stakes.map((s: { tokenId: string }) => s.tokenId)), + ].map((t) => BigNumber.from(t)) +} + +// Calculate unclamed rewards for each pool (currently 1, UBE/CELO) +async function getV3FarmPositionDefinitions( + networkId: NetworkId, + address: string, +): Promise { + return ( + await Promise.all( + FARM_POOLS.map(async (pool) => { + // Get incentive ids for pool + const incentiveIds = getIncentiveIdsByPool(pool) + + // Get staked tokenIds + const stakedTokenIds = await fetchStakedTokenIds(address, incentiveIds) + + const inputs = stakedTokenIds.map((tokenId) => [ + incentiveIds[0], + BigNumber.from(tokenId), + ]) + + if (!inputs.length) { + return + } + + // Fetch stakes from Farm contract + const stakes = (await client.readContract({ + address: FARM_ADDRESS, + abi: FarmAbi, + functionName: 'stakes', + args: inputs[0], + })) as number[] + + // Cast to StakeInfo type + const stakeInfos = [ + { + claimedReward: BigNumber.from(stakes[0]), + stakeTime: +stakes[1], + initialSecondsInside: +stakes[2], + }, + ] as StakeInfo[] + + // Fetch incentive data for UBE + const fullData = await fetchIncentiveFullData(incentiveIds[0]) + + // Get incentive and stake info for each token + const tokenDatas: TokenData[] = [] + for (let i = 0; i < stakedTokenIds.length; i++) { + const tokenId = BigNumber.from(stakedTokenIds[i]) + const incentiveData = fullData + ? fullData.find((d) => tokenId.eq(d.tokenId)) + : undefined + let stakeInfo + if (stakeInfos.length === stakedTokenIds.length && stakeInfos[i]) { + stakeInfo = { + claimedReward: stakeInfos[i]!.claimedReward, + stakeTime: stakeInfos[i]!.stakeTime, + initialSecondsInside: stakeInfos[i]!.initialSecondsInside, + } + } + tokenDatas.push({ + tokenId, + incentiveData, + stakeInfo, + }) + } + + // Calculate unclamed rewards (=accumulated-claimed) + const unclaimedRewards = tokenDatas + .filter((d) => d.incentiveData && d.stakeInfo) + .reduce((acc, curr) => { + return acc + .add(curr.incentiveData?.accumulatedRewards || 0) + .sub(curr.stakeInfo?.claimedReward || 0) + }, BigNumber.from(0)) + + const poolTokenContract = { + address: pool as `0x{string}`, + abi: uniswapV2PairAbi, + } as const + const [token0Address, token1Address] = await client.multicall({ + contracts: [ + { + ...poolTokenContract, + functionName: 'token0', + }, + { + ...poolTokenContract, + functionName: 'token1', + }, + ], + allowFailure: false, + }) + + return { + type: 'contract-position-definition', + networkId, + address: FARM_ADDRESS, + tokens: [token0Address, token1Address].map((token) => ({ + address: token.toLowerCase(), + networkId, + })), + balances: [toDecimalNumber(BigInt(+unclaimedRewards), UBE_DECIMALS)], + displayProps: ({ resolvedTokensByTokenId }) => { + const token0 = + resolvedTokensByTokenId[ + getTokenId({ + networkId, + address: token0Address, + }) + ] + const token1 = + resolvedTokensByTokenId[ + getTokenId({ + networkId, + address: token1Address, + }) + ] + return { + title: `${token0.symbol} / ${token1.symbol} 0.01% Farm`, + description: 'Staked position', + imageUrl: UBESWAP_LOGO, + manageUrl: `https://app.ubeswap.org/#/farmv3/${pool}`, + } + }, + } as PositionDefinition + }), + ) + ).filter((pd) => !!pd) +} + async function getFarmPositionDefinitions( networkId: NetworkId, address: string, @@ -264,7 +486,7 @@ async function getFarmPositionDefinitions( isNative: false, }) ] - const share = new BigNumber(farm.balance.toString()).div( + const share = new BigNumberJs(farm.balance.toString()).div( farm.totalSupply.toString(), ) @@ -336,15 +558,20 @@ const hook: PositionsHook = { // dapp is only on Celo, and implementation is hardcoded to Celo mainnet (contract addresses in particular) return [] } - const [poolDefinitions, farmDefinitions, v3Definitions] = await Promise.all( - [ + const [poolDefinitions, farmDefinitions, v3FarmDefinitions, v3Definitions] = + await Promise.all([ getPoolPositionDefinitions(networkId, address), getFarmPositionDefinitions(networkId, address), + getV3FarmPositionDefinitions(networkId, address), getV3Positions(networkId, address as Address), - ], - ) + ]) - return [...poolDefinitions, ...farmDefinitions, ...v3Definitions] + return [ + ...poolDefinitions, + ...farmDefinitions, + ...v3FarmDefinitions, + ...v3Definitions, + ] }, getAppTokenDefinition({ networkId, address }: TokenDefinition) { // Assume that the address is a pool address diff --git a/src/utils/numbers.ts b/src/utils/numbers.ts new file mode 100644 index 00000000..85c7db77 --- /dev/null +++ b/src/utils/numbers.ts @@ -0,0 +1,10 @@ +export const hexToUint8Array = (hex: string): Uint8Array => { + hex = hex.startsWith('0x') ? hex.substr(2) : hex + if (hex.length % 2 !== 0) + throw new Error('hex must have length that is multiple of 2') + const arr = new Uint8Array(hex.length / 2) + for (let i = 0; i < arr.length; i++) { + arr[i] = parseInt(hex.substr(i * 2, 2), 16) + } + return arr +}