@audius/sdk 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (278) hide show
  1. package/.eslintrc +38 -0
  2. package/.prettierrc.js +1 -0
  3. package/.python-version +1 -0
  4. package/Dockerfile +15 -0
  5. package/README.md +3 -0
  6. package/babel.config.js +3 -0
  7. package/data-contracts/ABIs/AdminUpgradeabilityProxy.json +132 -0
  8. package/data-contracts/ABIs/BaseAdminUpgradeabilityProxy.json +113 -0
  9. package/data-contracts/ABIs/BaseUpgradeabilityProxy.json +22 -0
  10. package/data-contracts/ABIs/DiscoveryProviderFactory.json +189 -0
  11. package/data-contracts/ABIs/DiscoveryProviderFactoryInterface.json +61 -0
  12. package/data-contracts/ABIs/DiscoveryProviderStorage.json +205 -0
  13. package/data-contracts/ABIs/DiscoveryProviderStorageInterface.json +65 -0
  14. package/data-contracts/ABIs/ECDSA.json +4 -0
  15. package/data-contracts/ABIs/IPLDBlacklistFactory.json +168 -0
  16. package/data-contracts/ABIs/Initializable.json +4 -0
  17. package/data-contracts/ABIs/Migrations.json +67 -0
  18. package/data-contracts/ABIs/OpenZeppelinUpgradesAddress.json +4 -0
  19. package/data-contracts/ABIs/Ownable.json +79 -0
  20. package/data-contracts/ABIs/PlaylistFactory.json +669 -0
  21. package/data-contracts/ABIs/PlaylistFactoryInterface.json +42 -0
  22. package/data-contracts/ABIs/PlaylistStorage.json +250 -0
  23. package/data-contracts/ABIs/PlaylistStorageInterface.json +129 -0
  24. package/data-contracts/ABIs/Proxy.json +10 -0
  25. package/data-contracts/ABIs/Registry.json +240 -0
  26. package/data-contracts/ABIs/RegistryContract.json +102 -0
  27. package/data-contracts/ABIs/RegistryContractInterface.json +28 -0
  28. package/data-contracts/ABIs/RegistryInterface.json +66 -0
  29. package/data-contracts/ABIs/SigningLogic.json +43 -0
  30. package/data-contracts/ABIs/SigningLogicInitializable.json +46 -0
  31. package/data-contracts/ABIs/SocialFeatureFactory.json +460 -0
  32. package/data-contracts/ABIs/SocialFeatureStorage.json +225 -0
  33. package/data-contracts/ABIs/SocialFeatureStorageInterface.json +123 -0
  34. package/data-contracts/ABIs/TestContract.json +135 -0
  35. package/data-contracts/ABIs/TestContractInterface.json +19 -0
  36. package/data-contracts/ABIs/TestContractWithStorage.json +165 -0
  37. package/data-contracts/ABIs/TestContractWithStorageInterface.json +24 -0
  38. package/data-contracts/ABIs/TestStorage.json +144 -0
  39. package/data-contracts/ABIs/TestStorageInterface.json +42 -0
  40. package/data-contracts/ABIs/TestUserReplicaSetManager.json +432 -0
  41. package/data-contracts/ABIs/TrackFactory.json +391 -0
  42. package/data-contracts/ABIs/TrackFactoryInterface.json +73 -0
  43. package/data-contracts/ABIs/TrackStorage.json +223 -0
  44. package/data-contracts/ABIs/TrackStorageInterface.json +121 -0
  45. package/data-contracts/ABIs/UpgradeabilityProxy.json +37 -0
  46. package/data-contracts/ABIs/UserFactory.json +657 -0
  47. package/data-contracts/ABIs/UserFactoryInterface.json +65 -0
  48. package/data-contracts/ABIs/UserLibraryFactory.json +334 -0
  49. package/data-contracts/ABIs/UserReplicaSetManager.json +418 -0
  50. package/data-contracts/ABIs/UserStorage.json +233 -0
  51. package/data-contracts/ABIs/UserStorageInterface.json +93 -0
  52. package/data-contracts/signatureSchemas.ts +1236 -0
  53. package/dist/core.d.ts +446 -0
  54. package/dist/core.js +769 -0
  55. package/dist/core.js.map +1 -0
  56. package/dist/index.d.ts +689 -0
  57. package/dist/index.js +72850 -0
  58. package/dist/index.js.map +1 -0
  59. package/eth-contracts/ABIs/Address.json +4 -0
  60. package/eth-contracts/ABIs/AudiusAdminUpgradeabilityProxy.json +105 -0
  61. package/eth-contracts/ABIs/AudiusClaimDistributor.json +4968 -0
  62. package/eth-contracts/ABIs/AudiusToken.json +724 -0
  63. package/eth-contracts/ABIs/BaseUpgradeabilityProxy.json +23 -0
  64. package/eth-contracts/ABIs/Checkpointing.json +4 -0
  65. package/eth-contracts/ABIs/ClaimsManager.json +539 -0
  66. package/eth-contracts/ABIs/Context.json +11 -0
  67. package/eth-contracts/ABIs/DelegateManager.json +989 -0
  68. package/eth-contracts/ABIs/DelegateManagerV2.json +1049 -0
  69. package/eth-contracts/ABIs/DelegateManagerV2Bad.json +1049 -0
  70. package/eth-contracts/ABIs/ERC20.json +252 -0
  71. package/eth-contracts/ABIs/ERC20Burnable.json +287 -0
  72. package/eth-contracts/ABIs/ERC20Detailed.json +270 -0
  73. package/eth-contracts/ABIs/ERC20Mintable.json +364 -0
  74. package/eth-contracts/ABIs/ERC20Pausable.json +397 -0
  75. package/eth-contracts/ABIs/EthRewardsManager.json +174 -0
  76. package/eth-contracts/ABIs/Governance.json +938 -0
  77. package/eth-contracts/ABIs/GovernanceUpgraded.json +953 -0
  78. package/eth-contracts/ABIs/GovernanceV2.json +938 -0
  79. package/eth-contracts/ABIs/IERC20.json +200 -0
  80. package/eth-contracts/ABIs/Initializable.json +4 -0
  81. package/eth-contracts/ABIs/InitializableV2.json +14 -0
  82. package/eth-contracts/ABIs/Migrations.json +71 -0
  83. package/eth-contracts/ABIs/MinterRole.json +91 -0
  84. package/eth-contracts/ABIs/MockAccount.json +62 -0
  85. package/eth-contracts/ABIs/MockDelegateManager.json +55 -0
  86. package/eth-contracts/ABIs/MockStakingCaller.json +259 -0
  87. package/eth-contracts/ABIs/MockWormhole.json +106 -0
  88. package/eth-contracts/ABIs/OpenZeppelinUpgradesAddress.json +4 -0
  89. package/eth-contracts/ABIs/Ownable.json +93 -0
  90. package/eth-contracts/ABIs/Pausable.json +150 -0
  91. package/eth-contracts/ABIs/PauserRole.json +91 -0
  92. package/eth-contracts/ABIs/Proxy.json +10 -0
  93. package/eth-contracts/ABIs/Registry.json +288 -0
  94. package/eth-contracts/ABIs/Roles.json +4 -0
  95. package/eth-contracts/ABIs/SafeERC20.json +4 -0
  96. package/eth-contracts/ABIs/SafeMath.json +4 -0
  97. package/eth-contracts/ABIs/ServiceProviderFactory.json +1153 -0
  98. package/eth-contracts/ABIs/ServiceTypeManager.json +337 -0
  99. package/eth-contracts/ABIs/Staking.json +555 -0
  100. package/eth-contracts/ABIs/StakingUpgraded.json +570 -0
  101. package/eth-contracts/ABIs/TestContract.json +44 -0
  102. package/eth-contracts/ABIs/TrustedNotifierManager.json +265 -0
  103. package/eth-contracts/ABIs/Uint256Helpers.json +4 -0
  104. package/eth-contracts/ABIs/UpgradeabilityProxy.json +40 -0
  105. package/eth-contracts/ABIs/Wormhole.json +45 -0
  106. package/eth-contracts/ABIs/WormholeClient.json +155 -0
  107. package/examples/file.mp3 +0 -0
  108. package/examples/initAudiusLibs.js +86 -0
  109. package/examples/initializeVersions.js +95 -0
  110. package/examples/pic.jpg +0 -0
  111. package/initScripts/configureLocalDiscProv.js +167 -0
  112. package/initScripts/helpers/claim.js +43 -0
  113. package/initScripts/helpers/distributeTokens.js +24 -0
  114. package/initScripts/helpers/spRegistration.js +138 -0
  115. package/initScripts/helpers/utils.js +34 -0
  116. package/initScripts/helpers/version.js +93 -0
  117. package/initScripts/local.js +617 -0
  118. package/initScripts/mainnet.js +131 -0
  119. package/initScripts/manageProdRelayerWallets.js +191 -0
  120. package/package.json +125 -0
  121. package/rollup.config.js +164 -0
  122. package/scripts/AudiusClaimDistributor.json +4968 -0
  123. package/scripts/Wormhole.json +155 -0
  124. package/scripts/addCIDToIpldBlacklist.js +124 -0
  125. package/scripts/circleci-test.sh +53 -0
  126. package/scripts/communityRewards/transferCommunityRewardsToSolana.js +222 -0
  127. package/scripts/ipfs.sh +58 -0
  128. package/scripts/migrate_contracts.sh +25 -0
  129. package/scripts/reset.sh +65 -0
  130. package/scripts/test.sh +77 -0
  131. package/src/api/account.js +670 -0
  132. package/src/api/base.js +122 -0
  133. package/src/api/file.js +168 -0
  134. package/src/api/playlist.js +328 -0
  135. package/src/api/rewards.d.ts +4 -0
  136. package/src/api/rewards.js +682 -0
  137. package/src/api/serviceProvider.js +154 -0
  138. package/src/api/track.js +604 -0
  139. package/src/api/user.js +888 -0
  140. package/src/api/user.test.js +172 -0
  141. package/src/constants.ts +7 -0
  142. package/src/core.ts +3 -0
  143. package/src/index.js +6 -0
  144. package/src/libs.d.ts +3 -0
  145. package/src/libs.js +619 -0
  146. package/src/sanityChecks/addSecondaries.js +40 -0
  147. package/src/sanityChecks/assignReplicaSetIfNecessary.js +10 -0
  148. package/src/sanityChecks/index.d.ts +9 -0
  149. package/src/sanityChecks/index.js +31 -0
  150. package/src/sanityChecks/isCreator.js +73 -0
  151. package/src/sanityChecks/needsRecoveryEmail.js +20 -0
  152. package/src/sanityChecks/rolloverNodes.js +74 -0
  153. package/src/sanityChecks/sanitizeNodes.js +24 -0
  154. package/src/sanityChecks/syncNodes.js +28 -0
  155. package/src/sdk/constants.ts +10 -0
  156. package/src/sdk/index.ts +1 -0
  157. package/src/sdk/oauth/Oauth.ts +265 -0
  158. package/src/sdk/oauth/index.ts +1 -0
  159. package/src/sdk/sdk.ts +102 -0
  160. package/src/service-selection/ServiceSelection.test.ts +320 -0
  161. package/src/service-selection/ServiceSelection.ts +460 -0
  162. package/src/service-selection/constants.ts +14 -0
  163. package/src/service-selection/index.ts +1 -0
  164. package/src/services/ABIDecoder/AudiusABIDecoder.ts +71 -0
  165. package/src/services/ABIDecoder/index.ts +1 -0
  166. package/src/services/comstock/Comstock.ts +39 -0
  167. package/src/services/comstock/index.ts +1 -0
  168. package/src/services/contracts/ContractClient.ts +227 -0
  169. package/src/services/contracts/GovernedContractClient.ts +53 -0
  170. package/src/services/contracts/ProviderSelection.ts +42 -0
  171. package/src/services/creatorNode/CreatorNode.ts +1065 -0
  172. package/src/services/creatorNode/CreatorNodeSelection.test.ts +997 -0
  173. package/src/services/creatorNode/CreatorNodeSelection.ts +488 -0
  174. package/src/services/creatorNode/constants.ts +10 -0
  175. package/src/services/creatorNode/index.ts +2 -0
  176. package/src/services/dataContracts/AudiusContracts.ts +234 -0
  177. package/src/services/dataContracts/IPLDBlacklistFactoryClient.ts +73 -0
  178. package/src/services/dataContracts/PlaylistFactoryClient.ts +370 -0
  179. package/src/services/dataContracts/RegistryClient.ts +95 -0
  180. package/src/services/dataContracts/SocialFeatureFactoryClient.ts +196 -0
  181. package/src/services/dataContracts/TrackFactoryClient.ts +131 -0
  182. package/src/services/dataContracts/UserFactoryClient.ts +351 -0
  183. package/src/services/dataContracts/UserLibraryFactoryClient.ts +115 -0
  184. package/src/services/dataContracts/UserReplicaSetManagerClient.ts +206 -0
  185. package/src/services/dataContracts/index.ts +1 -0
  186. package/src/services/discoveryProvider/DiscoveryProvider.ts +1168 -0
  187. package/src/services/discoveryProvider/DiscoveryProviderSelection.test.ts +536 -0
  188. package/src/services/discoveryProvider/DiscoveryProviderSelection.ts +383 -0
  189. package/src/services/discoveryProvider/constants.ts +13 -0
  190. package/src/services/discoveryProvider/index.ts +1 -0
  191. package/src/services/discoveryProvider/requests.ts +629 -0
  192. package/src/services/ethContracts/AudiusTokenClient.ts +163 -0
  193. package/src/services/ethContracts/ClaimDistributionClient.ts +45 -0
  194. package/src/services/ethContracts/ClaimsManagerClient.ts +102 -0
  195. package/src/services/ethContracts/DelegateManagerClient.ts +480 -0
  196. package/src/services/ethContracts/EthContracts.ts +359 -0
  197. package/src/services/ethContracts/EthRewardsManagerClient.ts +33 -0
  198. package/src/services/ethContracts/GovernanceClient.ts +451 -0
  199. package/src/services/ethContracts/RegistryClient.ts +33 -0
  200. package/src/services/ethContracts/ServiceProviderFactoryClient.ts +691 -0
  201. package/src/services/ethContracts/ServiceTypeManagerClient.ts +112 -0
  202. package/src/services/ethContracts/StakingProxyClient.ts +97 -0
  203. package/src/services/ethContracts/TrustedNotifierManagerClient.ts +101 -0
  204. package/src/services/ethContracts/WormholeClient.ts +97 -0
  205. package/src/services/ethContracts/index.ts +1 -0
  206. package/src/services/ethWeb3Manager/EthWeb3Manager.ts +239 -0
  207. package/src/services/ethWeb3Manager/index.ts +1 -0
  208. package/src/services/hedgehog/Hedgehog.ts +96 -0
  209. package/src/services/hedgehog/index.ts +1 -0
  210. package/src/services/identity/IdentityService.ts +551 -0
  211. package/src/services/identity/index.ts +1 -0
  212. package/src/services/identity/requests.ts +65 -0
  213. package/src/services/schemaValidator/SchemaValidator.ts +105 -0
  214. package/src/services/schemaValidator/index.ts +1 -0
  215. package/src/services/schemaValidator/schemas/trackSchema.json +267 -0
  216. package/src/services/schemaValidator/schemas/userSchema.json +230 -0
  217. package/src/services/solanaAudiusData/errors.ts +20 -0
  218. package/src/services/solanaAudiusData/index.ts +1189 -0
  219. package/src/services/solanaWeb3Manager/errors.js +101 -0
  220. package/src/services/solanaWeb3Manager/index.d.ts +46 -0
  221. package/src/services/solanaWeb3Manager/index.js +655 -0
  222. package/src/services/solanaWeb3Manager/padBNToUint8Array.ts +7 -0
  223. package/src/services/solanaWeb3Manager/rewards.js +941 -0
  224. package/src/services/solanaWeb3Manager/rewardsAttester.ts +1093 -0
  225. package/src/services/solanaWeb3Manager/tokenAccount.js +149 -0
  226. package/src/services/solanaWeb3Manager/transactionHandler.js +345 -0
  227. package/src/services/solanaWeb3Manager/transfer.js +272 -0
  228. package/src/services/solanaWeb3Manager/userBank.js +160 -0
  229. package/src/services/solanaWeb3Manager/utils.d.ts +31 -0
  230. package/src/services/solanaWeb3Manager/utils.js +163 -0
  231. package/src/services/solanaWeb3Manager/wAudio.js +28 -0
  232. package/src/services/solanaWeb3Manager/wAudio.test.js +30 -0
  233. package/src/services/web3Manager/Web3Config.ts +14 -0
  234. package/src/services/web3Manager/Web3Manager.ts +360 -0
  235. package/src/services/web3Manager/XMLHttpRequest.ts +11 -0
  236. package/src/services/web3Manager/index.ts +2 -0
  237. package/src/services/wormhole/index.js +424 -0
  238. package/src/types.ts +8 -0
  239. package/src/userStateManager.ts +53 -0
  240. package/src/utils/apiSigning.ts +51 -0
  241. package/src/utils/captcha.ts +97 -0
  242. package/src/utils/estimateGas.ts +64 -0
  243. package/src/utils/fileHasher.ts +278 -0
  244. package/src/utils/importContractABI.d.ts +9 -0
  245. package/src/utils/importContractABI.js +19 -0
  246. package/src/utils/index.ts +11 -0
  247. package/src/utils/multiProvider.ts +72 -0
  248. package/src/utils/network.test.ts +127 -0
  249. package/src/utils/network.ts +308 -0
  250. package/src/utils/promiseFight.test.ts +87 -0
  251. package/src/utils/promiseFight.ts +36 -0
  252. package/src/utils/signatures.ts +139 -0
  253. package/src/utils/types.ts +34 -0
  254. package/src/utils/utils.test.ts +36 -0
  255. package/src/utils/utils.ts +235 -0
  256. package/src/utils/uuid.ts +14 -0
  257. package/src/web3.d.ts +9 -0
  258. package/src/web3.js +8 -0
  259. package/tests/assets/static_image.png +0 -0
  260. package/tests/assets/static_text.txt +1 -0
  261. package/tests/audiusTokenClientTest.js +37 -0
  262. package/tests/creatorNodeTest.js +19 -0
  263. package/tests/fileHasherTest.js +125 -0
  264. package/tests/governanceTest.js +382 -0
  265. package/tests/helpers.js +105 -0
  266. package/tests/index.js +14 -0
  267. package/tests/playlistClientTest.js +157 -0
  268. package/tests/providerSelectionTest.js +241 -0
  269. package/tests/registryClientTest.js +19 -0
  270. package/tests/rewardsAttesterTest.js +373 -0
  271. package/tests/serviceTypeManagerClientTest.js +33 -0
  272. package/tests/socialFeatureClientTest.js +79 -0
  273. package/tests/stakingTest.js +302 -0
  274. package/tests/trackClientTest.js +86 -0
  275. package/tests/userClientTest.js +121 -0
  276. package/tsconfig.json +10 -0
  277. package/types/@audius-hedgehog/index.d.ts +39 -0
  278. package/types/abi-decoder/index.d.ts +41 -0
@@ -0,0 +1,1093 @@
1
+ import { SubmitAndEvaluateError } from '../../api/rewards'
2
+ import type { ServiceWithEndpoint } from '../../utils'
3
+ import { Utils } from '../../utils/utils'
4
+
5
+ const { decodeHashId } = Utils
6
+
7
+ // `BaseRewardsReporter` is intended to be subclassed, and provides
8
+ // "reporting" functionality to RewardsAttester (i.e. posts to Slack if something notable happens)
9
+ class BaseRewardsReporter {
10
+ async reportSuccess(_: {
11
+ userId: number
12
+ challengeId: string
13
+ amount: number
14
+ specifier: string
15
+ }): Promise<void> {}
16
+
17
+ async reportRetry(_: {
18
+ userId: number
19
+ challengeId: string
20
+ amount: number
21
+ error: string
22
+ phase: string
23
+ }): Promise<void> {}
24
+
25
+ async reportFailure(_: {
26
+ userId: number
27
+ challengeId: string
28
+ amount: number
29
+ error: string
30
+ phase: string
31
+ }): Promise<void> {}
32
+
33
+ async reportAAORejection(_: {
34
+ userId: number
35
+ challengeId: string
36
+ amount: number
37
+ error: string
38
+ reason: string
39
+ }): Promise<void> {}
40
+ }
41
+
42
+ const MAX_DISBURSED_CACHE_SIZE = 100
43
+ const SOLANA_EST_SEC_PER_SLOT = 0.5
44
+ const POA_SEC_PER_BLOCK = 5
45
+ const MAX_DISCOVERY_NODE_BLOCKLIST_LEN = 10
46
+
47
+ type ATTESTER_PHASE =
48
+ | 'HALTED'
49
+ | 'SELECTING_NODES'
50
+ | 'REFILLING_QUEUE'
51
+ | 'ATTESTING'
52
+ | 'SLEEPING'
53
+ | 'RETRY_BACKOFF'
54
+
55
+ /**
56
+ * Class to encapsulate logic for calculating disbursement delay thresholds.
57
+ * Periodically polls Solana to get slot production rate.
58
+ * Caches old values (`allowedStalenessSec`) for current POA block & Solana slot to reduce RPC
59
+ * overhead.
60
+ *
61
+ * Exposes `getPOABlockThreshold` and `getSolanaSlotThreshold`
62
+ *
63
+ * @class ThresholdCalculator
64
+ */
65
+ export class AttestationDelayCalculator {
66
+ libs: any
67
+ solanaSecPerSlot: number
68
+ runBehindSec: number
69
+ lastSolanaThreshold: { threshold: number; time: number } | null
70
+ lastPOAThreshold: { threshold: number; time: number } | null
71
+ allowedStalenessSec: number
72
+ solanaPollingInterval: number
73
+ logger: any
74
+ intervalHandle: NodeJS.Timer | null
75
+
76
+ constructor({
77
+ libs,
78
+ runBehindSec,
79
+ allowedStalenessSec,
80
+ solanaPollingInterval = 30,
81
+ logger = console
82
+ }: {
83
+ libs: any
84
+ runBehindSec: number
85
+ allowedStalenessSec: number
86
+ solanaPollingInterval?: number
87
+ logger: any
88
+ }) {
89
+ this.libs = libs
90
+ this.solanaSecPerSlot = SOLANA_EST_SEC_PER_SLOT
91
+ this.runBehindSec = runBehindSec
92
+ this.lastSolanaThreshold = null
93
+ this.lastPOAThreshold = null
94
+ this.allowedStalenessSec = allowedStalenessSec
95
+ this.solanaPollingInterval = solanaPollingInterval
96
+ this.logger = logger
97
+ this.intervalHandle = null
98
+ }
99
+
100
+ async start() {
101
+ // Begin Solana slot rate polling
102
+ let oldSlot = await this.libs.solanaWeb3Manager.getSlot()
103
+ // eslint-disable-next-line
104
+ this.intervalHandle = setInterval(async () => {
105
+ const newSlot = await this.libs.solanaWeb3Manager.getSlot()
106
+ const diff = this.solanaPollingInterval / (newSlot - oldSlot)
107
+ this.solanaSecPerSlot = diff
108
+ this.logger.info(`Setting Solana seconds per slot to ${diff}`)
109
+ oldSlot = newSlot
110
+ }, this.solanaPollingInterval * 1000)
111
+ }
112
+
113
+ stop() {
114
+ if (this.intervalHandle) {
115
+ clearInterval(this.intervalHandle)
116
+ }
117
+ }
118
+
119
+ async getPOABlockThreshold() {
120
+ // Use cached value if possible
121
+ if (
122
+ this.lastPOAThreshold &&
123
+ (Date.now() - this.lastPOAThreshold.time) / 1000 <
124
+ this.allowedStalenessSec
125
+ ) {
126
+ return this.lastPOAThreshold.threshold
127
+ }
128
+ const currentBlock = await this.libs.web3Manager
129
+ .getWeb3()
130
+ .eth.getBlockNumber()
131
+ const threshold = currentBlock - this.runBehindSec / POA_SEC_PER_BLOCK
132
+ this.lastPOAThreshold = {
133
+ threshold,
134
+ time: Date.now()
135
+ }
136
+ return threshold
137
+ }
138
+
139
+ async getSolanaSlotThreshold() {
140
+ // Use cached value if possible
141
+ if (
142
+ this.lastSolanaThreshold &&
143
+ (Date.now() - this.lastSolanaThreshold.time) / 1000 <
144
+ this.allowedStalenessSec
145
+ ) {
146
+ return this.lastSolanaThreshold.threshold
147
+ }
148
+ const currentSlot = await this.libs.solanaWeb3Manager.getSlot()
149
+ const threshold = currentSlot - this.runBehindSec / this.solanaSecPerSlot
150
+ this.lastSolanaThreshold = {
151
+ threshold,
152
+ time: Date.now()
153
+ }
154
+ return threshold
155
+ }
156
+ }
157
+
158
+ type ConstructorArgs = {
159
+ libs: any
160
+ startingBlock: number
161
+ offset: number
162
+ parallelization: number
163
+ logger?: any
164
+ quorumSize: number
165
+ aaoEndpoint: string
166
+ aaoAddress: string
167
+ updateValues: ({
168
+ startingBlock,
169
+ offset,
170
+ successCount
171
+ }: {
172
+ startingBlock: number
173
+ offset: number
174
+ successCount: number
175
+ }) => void
176
+ getStartingBlockOverride: () => Promise<number | null> | number | null
177
+ maxRetries: number
178
+ reporter?: BaseRewardsReporter
179
+ challengeIdsDenyList: string[]
180
+ endpoints?: string[]
181
+ runBehindSec?: number
182
+ isSolanaChallenge?: (challenge: string) => boolean
183
+ feePayerOverride: string | null
184
+ maxAggregationAttempts?: number
185
+ updateStateCallback?: (state: AttesterState) => Promise<void>
186
+ maxCooldownMsec?: number
187
+ }
188
+
189
+ type Challenge = {
190
+ challengeId: string
191
+ userId: string
192
+ specifier: string
193
+ amount: number
194
+ handle: string
195
+ wallet: string
196
+ completedBlocknumber: number
197
+ }
198
+
199
+ type AttestationResult = Challenge & {
200
+ error?: string
201
+ phase?: string
202
+ nodesToReselect?: string[] | null
203
+ }
204
+
205
+ type DiscoveryNodeChallenge = {
206
+ challenge_id: string
207
+ user_id: string
208
+ specifier: string
209
+ amount: number
210
+ handle: string
211
+ wallet: string
212
+ completed_blocknumber: number
213
+ }
214
+
215
+ type AttesterState = {
216
+ phase: ATTESTER_PHASE
217
+ lastSuccessChallengeTime: number | null
218
+ lastChallengeTime: number | null
219
+ lastActionTime: number
220
+ }
221
+
222
+ /**
223
+ * `RewardsAttester` is responsible for repeatedly attesting for completed rewards.
224
+ *
225
+ * **Implementation**
226
+ *
227
+ * `RewardsAttester` attempts to attest for `parallelization` rewards in parallel.
228
+ * It won't move onto the next batch of rewards until every reward in that batch has
229
+ * either succeeded or failed attestation. It retries errors that might be due to DN
230
+ * timing issues, and skips AAO errors and some Solana program errors.
231
+ *
232
+ * Internally, state is tracked with two variables: `offset` and `startingBlock`.
233
+ * `startingBlock` represents which block it start requesting attestations from, while `offset` determines
234
+ * where within those results we offset. AAO rejected rewards
235
+ * are never cleared from the DN rewards queue, so we have to move past them either with `offset` or `startingBlock`.
236
+ * `RewardsAttester` accepts callbacks (`updateValues`) for a client to persist these values periodically.
237
+ *
238
+ * RewardsAttester will fetch a single large list of undisbursed rewards (`undisbursedQueue`), and
239
+ * process that entire list before fetching new undisbursed rewards. It also maintains a list of
240
+ * recently processed rewards, and filters those out when re-fetching new undisbursed rewards.
241
+ */
242
+ export class RewardsAttester {
243
+ private startingBlock: number
244
+ private offset: number
245
+ // Stores a set of identifiers representing
246
+ // recently disbursed challenges.
247
+ // Stored as an array to make it simpler to prune
248
+ // old entries
249
+ private recentlyDisbursedQueue: string[]
250
+ private _shouldStop: boolean
251
+ private endpoints: string[]
252
+ // Stores a queue of undisbursed challenges
253
+ private undisbursedQueue: Challenge[]
254
+ private attesterState: AttesterState
255
+ private parallelization: number
256
+ private aaoEndpoint: string
257
+ private aaoAddress: string
258
+ private endpointPool: Set<string>
259
+ private challengeIdsDenyList: Set<string>
260
+ private discoveryNodeBlocklist: string[]
261
+
262
+ private readonly libs: any
263
+ private readonly logger: Console
264
+ private readonly quorumSize: number
265
+ private readonly reporter: BaseRewardsReporter
266
+ private readonly maxRetries: number
267
+ private readonly maxAggregationAttempts: number
268
+ private readonly updateValues: (args: {
269
+ startingBlock: number
270
+ offset: number
271
+ successCount: number
272
+ }) => void
273
+
274
+ // How long wait wait before retrying
275
+ private readonly cooldownMsec: number
276
+ // How much we increase the cooldown between attempts:
277
+ // coolDown = min(cooldownMsec * backoffExponent ^ retryCount, maxCooldownMsec)
278
+ private readonly backoffExponent: number
279
+ // Maximum time to wait before retrying
280
+ private readonly maxCooldownMsec: number
281
+ // Maximum number of retries before moving on
282
+ // Get override starting block for manually setting indexing start
283
+ private readonly getStartingBlockOverride: () =>
284
+ | Promise<number | null>
285
+ | number
286
+ | null
287
+
288
+ private readonly feePayerOverride: string | null
289
+
290
+ // Calculate delay
291
+ private readonly delayCalculator: AttestationDelayCalculator
292
+ private readonly isSolanaChallenge: (challenge: string) => boolean
293
+ private readonly _updateStateCallback: (state: AttesterState) => Promise<void>
294
+
295
+ /**
296
+ * Creates an instance of RewardsAttester.
297
+ * @memberof RewardsAttester
298
+ */
299
+ constructor({
300
+ libs,
301
+ startingBlock,
302
+ offset,
303
+ parallelization,
304
+ logger = console,
305
+ quorumSize,
306
+ aaoEndpoint,
307
+ aaoAddress,
308
+ updateValues = () => {},
309
+ getStartingBlockOverride = () => null,
310
+ maxRetries = 5,
311
+ reporter = new BaseRewardsReporter(),
312
+ challengeIdsDenyList = [],
313
+ endpoints = [],
314
+ runBehindSec = 0,
315
+ isSolanaChallenge = (_) => true,
316
+ feePayerOverride = null,
317
+ maxAggregationAttempts = 20,
318
+ updateStateCallback = async (_) => {},
319
+ maxCooldownMsec = 15000
320
+ }: ConstructorArgs) {
321
+ this.libs = libs
322
+ this.logger = logger
323
+ this.parallelization = parallelization
324
+ this.startingBlock = startingBlock
325
+ this.offset = offset
326
+ this.quorumSize = quorumSize
327
+ this.aaoEndpoint = aaoEndpoint
328
+ this.aaoAddress = aaoAddress
329
+ this.reporter = reporter
330
+ this.endpoints = endpoints
331
+ this.endpointPool = new Set(endpoints)
332
+ this.maxRetries = maxRetries
333
+ this.maxAggregationAttempts = maxAggregationAttempts
334
+ this.updateValues = updateValues
335
+ this.challengeIdsDenyList = new Set(...challengeIdsDenyList)
336
+ this.undisbursedQueue = []
337
+ this.recentlyDisbursedQueue = []
338
+ this.cooldownMsec = 2000
339
+ this.backoffExponent = 1.8
340
+ this.maxCooldownMsec = maxCooldownMsec
341
+ this.getStartingBlockOverride = getStartingBlockOverride
342
+ this.feePayerOverride = feePayerOverride
343
+ this.attesterState = {
344
+ phase: 'HALTED',
345
+ lastSuccessChallengeTime: null,
346
+ lastChallengeTime: null,
347
+ lastActionTime: Date.now()
348
+ }
349
+
350
+ // Calculate delay
351
+ this.delayCalculator = new AttestationDelayCalculator({
352
+ libs,
353
+ runBehindSec,
354
+ logger,
355
+ allowedStalenessSec: 5
356
+ })
357
+ this.isSolanaChallenge = isSolanaChallenge
358
+
359
+ this._performSingleAttestation = this._performSingleAttestation.bind(this)
360
+ this._disbursementToKey = this._disbursementToKey.bind(this)
361
+ this._shouldStop = false
362
+ this._updateStateCallback = updateStateCallback
363
+ this.discoveryNodeBlocklist = []
364
+ }
365
+
366
+ /**
367
+ * Begin attestation loop. Entry point for identity attestations
368
+ *
369
+ * @memberof RewardsAttester
370
+ */
371
+ async start() {
372
+ this.logger.info(`Starting attester with:
373
+ quorum size: ${this.quorumSize}, \
374
+ parallelization: ${this.parallelization} \
375
+ AAO endpoint: ${this.aaoEndpoint} \
376
+ AAO address: ${this.aaoAddress} \
377
+ endpoints: ${this.endpoints}
378
+ `)
379
+
380
+ // If a list of endpoints was not specified,
381
+ // set the pool to the entire list of discovery providers.
382
+ // This overrides any configured whitelist for the service selector.
383
+ if (this.endpointPool.size === 0) {
384
+ const pool =
385
+ await this.libs.discoveryProvider.serviceSelector.getServices()
386
+ this.endpointPool = new Set(pool)
387
+ }
388
+ await this._selectDiscoveryNodes()
389
+ await this.delayCalculator.start()
390
+
391
+ while (!this._shouldStop) {
392
+ try {
393
+ await this._awaitFeePayerBalance()
394
+ await this._checkForStartingBlockOverride()
395
+
396
+ // Refill queue if necessary, returning early if error
397
+ const { error } = await this._refillQueueIfNecessary()
398
+ if (error) {
399
+ this.logger.error(`Got error trying to refill challenges: [${error}]`)
400
+ throw new Error(error)
401
+ }
402
+
403
+ // If queue is still empty, sleep and return
404
+ if (!this.undisbursedQueue.length) {
405
+ this.logger.info('No undisbursed challenges. Sleeping...')
406
+ await this._updatePhase('SLEEPING')
407
+ await this._delay(1000)
408
+ continue
409
+ }
410
+
411
+ // Get undisbursed rewards
412
+ const toAttest = this.undisbursedQueue.splice(0, this.parallelization)
413
+
414
+ // Attest for batch in parallel
415
+ const { highestBlock, offset, successCount } =
416
+ await this._attestInParallel(toAttest)
417
+
418
+ // Set state
419
+ // Set offset:
420
+ // - If same startingBlock as before, add offset
421
+ // - If new startingBlock, set offset
422
+ if (highestBlock && this.startingBlock === highestBlock - 1) {
423
+ this.offset += offset
424
+ } else {
425
+ this.offset = offset
426
+ }
427
+
428
+ this.logger.info(
429
+ `Updating values: startingBlock: ${this.startingBlock}, offset: ${this.offset}`
430
+ )
431
+
432
+ this.startingBlock = highestBlock
433
+ ? highestBlock - 1
434
+ : this.startingBlock
435
+
436
+ // Set the recently disbursed set
437
+ this._addRecentlyDisbursed(toAttest)
438
+
439
+ // run the `updateValues` callback
440
+ await this.updateValues({
441
+ startingBlock: this.startingBlock,
442
+ offset: this.offset,
443
+ successCount
444
+ })
445
+ } catch (e) {
446
+ this.logger.error(`Got error: ${e}, sleeping`)
447
+ await this._delay(1000)
448
+ }
449
+ }
450
+
451
+ this._shouldStop = false
452
+ }
453
+
454
+ async stop() {
455
+ this._shouldStop = true
456
+ this.delayCalculator.stop()
457
+ }
458
+
459
+ /**
460
+ * Called from the client to attest challenges
461
+ */
462
+ async processChallenges(challenges: Challenge[]) {
463
+ await this._selectDiscoveryNodes()
464
+ const toProcess = [...challenges]
465
+ while (toProcess.length) {
466
+ try {
467
+ this.logger.info(`Processing ${toProcess.length} challenges`)
468
+ const toAttest = toProcess.splice(0, this.parallelization)
469
+ const { accumulatedErrors: errors } = await this._attestInParallel(
470
+ toAttest
471
+ )
472
+ if (errors?.length) {
473
+ this.logger.error(
474
+ `Got errors in processChallenges: ${JSON.stringify(errors)}`
475
+ )
476
+ return { errors }
477
+ }
478
+ } catch (e) {
479
+ this.logger.error(`Got error: ${e}, sleeping`)
480
+ await this._delay(1000)
481
+ }
482
+ }
483
+ return {}
484
+ }
485
+
486
+ /**
487
+ * Updates attester config
488
+ *
489
+ * @memberof RewardsAttester
490
+ */
491
+ updateConfig({
492
+ aaoEndpoint,
493
+ aaoAddress,
494
+ endpoints,
495
+ challengeIdsDenyList,
496
+ parallelization
497
+ }: {
498
+ aaoEndpoint: string
499
+ aaoAddress: string
500
+ endpoints: string[]
501
+ challengeIdsDenyList: string[]
502
+ parallelization: number
503
+ }) {
504
+ this.logger.info(
505
+ `Updating attester with config aaoEndpoint: ${aaoEndpoint}, aaoAddress: ${aaoAddress}, endpoints: ${endpoints}, challengeIdsDenyList: ${challengeIdsDenyList}, parallelization: ${parallelization}`
506
+ )
507
+ this.aaoEndpoint = aaoEndpoint || this.aaoEndpoint
508
+ this.aaoAddress = aaoAddress || this.aaoAddress
509
+ this.endpoints = endpoints || this.endpoints
510
+ this.challengeIdsDenyList = challengeIdsDenyList
511
+ ? new Set(...challengeIdsDenyList)
512
+ : this.challengeIdsDenyList
513
+ this.parallelization = parallelization || this.parallelization
514
+ }
515
+
516
+ /**
517
+ * Sleeps until the feePayer has a usable Sol balance.
518
+ *
519
+ * @memberof RewardsAttester
520
+ */
521
+ async _awaitFeePayerBalance() {
522
+ const getHasBalance = async () =>
523
+ this.libs.solanaWeb3Manager.hasBalance({
524
+ publicKey: this.libs.solanaWeb3Manager.feePayerKey
525
+ })
526
+ while (!(await getHasBalance())) {
527
+ this.logger.warn('No usable balance. Waiting...')
528
+ await this._delay(2000)
529
+ }
530
+ }
531
+
532
+ /**
533
+ * Returns the override feePayer if set, otherwise a random fee payer from among the list of existing fee payers.
534
+ *
535
+ * @memberof RewardsAttester
536
+ */
537
+ _getFeePayer() {
538
+ if (this.feePayerOverride) {
539
+ return this.feePayerOverride
540
+ }
541
+ const feePayerKeypairs =
542
+ this.libs.solanaWeb3Manager.solanaWeb3Config.feePayerKeypairs
543
+ if (feePayerKeypairs?.length) {
544
+ const randomFeePayerIndex = Math.floor(
545
+ Math.random() * feePayerKeypairs.length
546
+ )
547
+ return feePayerKeypairs[randomFeePayerIndex].publicKey
548
+ }
549
+ return null
550
+ }
551
+
552
+ /**
553
+ * Escape hatch for manually setting starting block.
554
+ *
555
+ * @memberof RewardsAttester
556
+ */
557
+ async _checkForStartingBlockOverride() {
558
+ const override = await this.getStartingBlockOverride()
559
+ // Careful with 0...
560
+ if (override === null || override === undefined) return
561
+ this.logger.info(
562
+ `Setting starting block override: ${override}, emptying recent disbursed queue`
563
+ )
564
+ this.startingBlock = override
565
+ this.offset = 0
566
+ this.recentlyDisbursedQueue = []
567
+ this.undisbursedQueue = []
568
+ this.discoveryNodeBlocklist = []
569
+ }
570
+
571
+ /**
572
+ * Main method to attest for a bucket of challenges in parallel.
573
+ *
574
+ * Algorithm:
575
+ * - Gets `this.parallelization` undisbursed challenges from the queue, refilling it from DN if necessary.
576
+ * - Call `_performSingleAttestation` on those in parallel.
577
+ * - For challenges that failed, either keep retrying or discard them, depending on the error.
578
+ * - Set offset and startingBlock
579
+ *
580
+ * @memberof RewardsAttester
581
+ */
582
+ async _attestInParallel(toAttest: Challenge[]) {
583
+ this.logger.info(
584
+ `Attesting in parallel with startingBlock: ${this.startingBlock}, offset: ${this.offset}, parallelization: ${this.parallelization}`
585
+ )
586
+ await this._updatePhase('ATTESTING')
587
+ // Get the highest block number, ignoring Solana based challenges (i.e. listens) which have a significantly higher
588
+ // slot and throw off this calculation.
589
+ // TODO: [AUD-1217] we should handle this in a less hacky way, possibly by
590
+ // attesting for Solana + POA challenges separately.
591
+ const poaAttestations = toAttest.filter(
592
+ ({ challengeId }) => !this.isSolanaChallenge(challengeId)
593
+ )
594
+ const highestBlock = poaAttestations.length
595
+ ? Math.max(...poaAttestations.map((e) => e.completedBlocknumber))
596
+ : null
597
+
598
+ let retryCount = 0
599
+ let successful: AttestationResult[] = []
600
+ let noRetry: AttestationResult[] = []
601
+ let needsAttestation: AttestationResult[] = toAttest
602
+ let shouldReselect = false
603
+ let accumulatedErrors: AttestationResult[] = []
604
+ let successCount = 0
605
+ let offset = 0
606
+ let failingNodes: string[] = []
607
+
608
+ do {
609
+ // Attempt to attest in a single sweep
610
+ await this._updatePhase('ATTESTING')
611
+ if (retryCount !== 0) {
612
+ await this._backoff(retryCount)
613
+ }
614
+
615
+ this.logger.info(
616
+ `Attestation attempt ${retryCount + 1}, max ${this.maxRetries}`
617
+ )
618
+
619
+ if (shouldReselect) {
620
+ await this._selectDiscoveryNodes()
621
+ }
622
+
623
+ const results = await Promise.all(
624
+ needsAttestation.map(this._performSingleAttestation)
625
+ )
626
+
627
+ // "Process" the results of attestation into noRetry and needsAttestation errors,
628
+ // as well as a flag that indicates whether we should reselect.
629
+ ;({
630
+ successful,
631
+ noRetry,
632
+ needsRetry: needsAttestation,
633
+ shouldReselect,
634
+ failingNodes
635
+ } = await this._processResponses(
636
+ results,
637
+ retryCount === this.maxRetries - 1
638
+ ))
639
+
640
+ // Add failing nodes to the blocklist, trimming out oldest nodes if necessary
641
+ if (failingNodes?.length) {
642
+ const existing = new Set(this.discoveryNodeBlocklist)
643
+ failingNodes.forEach((n) => {
644
+ if (!existing.has(n)) {
645
+ this.discoveryNodeBlocklist.push(n)
646
+ }
647
+ })
648
+ this.discoveryNodeBlocklist = this.discoveryNodeBlocklist.slice(
649
+ -1 * MAX_DISCOVERY_NODE_BLOCKLIST_LEN
650
+ )
651
+ }
652
+
653
+ successCount += successful.length
654
+ accumulatedErrors = [...accumulatedErrors, ...noRetry]
655
+
656
+ // Increment offset by the # of errors we're not retrying that have the max block #.
657
+ //
658
+ // Note: any successfully completed rewards will eventually be flushed from the
659
+ // disbursable queue on DN, but ignored rewards will stay stuck in that list, so we
660
+ // have to move past them with offset if they're not already moved past with `startingBlock`.
661
+ offset += noRetry.filter(
662
+ ({ completedBlocknumber }) => completedBlocknumber === highestBlock
663
+ ).length
664
+
665
+ retryCount++
666
+ } while (needsAttestation.length && retryCount < this.maxRetries)
667
+
668
+ if (retryCount === this.maxRetries) {
669
+ this.logger.error(`Gave up with ${retryCount} retries`)
670
+ }
671
+
672
+ return {
673
+ accumulatedErrors,
674
+ highestBlock,
675
+ offset,
676
+ successCount
677
+ }
678
+ }
679
+
680
+ /**
681
+ * Attempts to attest for a single challenge.
682
+ *
683
+ * @memberof RewardsAttester
684
+ */
685
+ async _performSingleAttestation({
686
+ challengeId,
687
+ userId,
688
+ specifier,
689
+ amount,
690
+ handle,
691
+ wallet,
692
+ completedBlocknumber
693
+ }: Challenge): Promise<AttestationResult> {
694
+ this.logger.info(
695
+ `Attempting to attest for userId [${decodeHashId(
696
+ userId
697
+ )}], challengeId: [${challengeId}], quorum size: [${this.quorumSize}]}`
698
+ )
699
+
700
+ const { success, error, phase, nodesToReselect } =
701
+ await this.libs.Rewards.submitAndEvaluate({
702
+ challengeId,
703
+ encodedUserId: userId,
704
+ handle,
705
+ recipientEthAddress: wallet,
706
+ specifier,
707
+ oracleEthAddress: this.aaoAddress,
708
+ amount,
709
+ quorumSize: this.quorumSize,
710
+ AAOEndpoint: this.aaoEndpoint,
711
+ endpoints: this.endpoints,
712
+ logger: this.logger,
713
+ feePayerOverride: this._getFeePayer(),
714
+ maxAggregationAttempts: this.maxAggregationAttempts
715
+ })
716
+
717
+ if (success) {
718
+ this.logger.info(
719
+ `Successfully attestested for challenge [${challengeId}] for user [${decodeHashId(
720
+ userId
721
+ )}], amount [${amount}]!`
722
+ )
723
+ return {
724
+ challengeId,
725
+ userId,
726
+ specifier,
727
+ amount,
728
+ handle,
729
+ wallet,
730
+ completedBlocknumber,
731
+ nodesToReselect: null
732
+ }
733
+ }
734
+
735
+ // Handle error path
736
+ this.logger.error(
737
+ `Failed to attest for challenge [${challengeId}] for user [${decodeHashId(
738
+ userId
739
+ )}], amount [${amount}], oracle: [${
740
+ this.aaoAddress
741
+ }] at phase: [${phase}] with error [${error}]`
742
+ )
743
+
744
+ return {
745
+ challengeId,
746
+ userId,
747
+ specifier,
748
+ amount,
749
+ handle,
750
+ wallet,
751
+ completedBlocknumber,
752
+ error,
753
+ phase,
754
+ nodesToReselect
755
+ }
756
+ }
757
+
758
+ async _selectDiscoveryNodes() {
759
+ await this._updatePhase('SELECTING_NODES')
760
+ this.logger.info(
761
+ `Selecting discovery nodes with blocklist ${JSON.stringify(
762
+ this.discoveryNodeBlocklist
763
+ )}`
764
+ )
765
+ const startTime = Date.now()
766
+ let endpoints: ServiceWithEndpoint[] =
767
+ (await this.libs.discoveryProvider.serviceSelector.findAll({
768
+ verbose: true,
769
+ whitelist: this.endpointPool.size > 0 ? this.endpointPool : null
770
+ })) ?? []
771
+ // Filter out blocklisted nodes
772
+ const blockSet = new Set(this.discoveryNodeBlocklist)
773
+ endpoints = [...endpoints].filter((e) => !blockSet.has(e.endpoint))
774
+
775
+ this.endpoints =
776
+ await this.libs.Rewards.ServiceProvider.getUniquelyOwnedDiscoveryNodes(
777
+ this.quorumSize,
778
+ endpoints
779
+ )
780
+ this.logger.info(
781
+ `Selected new discovery nodes in ${
782
+ (Date.now() - startTime) / 1000
783
+ } seconds: [${this.endpoints}]`
784
+ )
785
+ }
786
+
787
+ /**
788
+ * Fetches new undisbursed rewards and inserts them into the undisbursedQueue
789
+ * if the queue is currently empty.
790
+ *
791
+ * @memberof RewardsAttester
792
+ */
793
+ async _refillQueueIfNecessary() {
794
+ if (this.undisbursedQueue.length) return {}
795
+
796
+ this.logger.info(
797
+ `Refilling queue with startingBlock: ${this.startingBlock}, offset: ${
798
+ this.offset
799
+ }, recently disbursed: ${JSON.stringify(this.recentlyDisbursedQueue)}`
800
+ )
801
+ await this._updatePhase('REFILLING_QUEUE')
802
+ const {
803
+ success: disbursable,
804
+ error
805
+ }: { success: DiscoveryNodeChallenge[]; error: null } =
806
+ await this.libs.Rewards.getUndisbursedChallenges({
807
+ offset: this.offset,
808
+ completedBlockNumber: this.startingBlock,
809
+ logger: this.logger
810
+ })
811
+
812
+ if (error) {
813
+ return { error }
814
+ }
815
+
816
+ if (disbursable?.length) {
817
+ this.logger.info(
818
+ `Got challenges: ${disbursable.map(
819
+ (
820
+ { challenge_id, user_id, specifier } // eslint-disable-line
821
+ ) => `${challenge_id}-${user_id}-${specifier}`
822
+ )}`
823
+ ) // eslint-disable-line
824
+ }
825
+
826
+ // Map to camelCase, and filter out
827
+ // any challenges in the denylist or recently disbursed set
828
+ this.undisbursedQueue = disbursable
829
+ .map(
830
+ ({
831
+ challenge_id, // eslint-disable-line
832
+ user_id, // eslint-disable-line
833
+ specifier,
834
+ amount,
835
+ handle,
836
+ wallet,
837
+ completed_blocknumber // eslint-disable-line
838
+ }) => ({
839
+ challengeId: challenge_id,
840
+ userId: user_id,
841
+ specifier,
842
+ amount,
843
+ handle,
844
+ wallet,
845
+ completedBlocknumber: completed_blocknumber
846
+ })
847
+ )
848
+ .filter(
849
+ (d) =>
850
+ !(
851
+ this.challengeIdsDenyList.has(d.challengeId) ||
852
+ new Set(this.recentlyDisbursedQueue).has(this._disbursementToKey(d))
853
+ )
854
+ )
855
+
856
+ // Filter out recently disbursed challenges
857
+ if (this.undisbursedQueue.length) {
858
+ this.undisbursedQueue = await this._filterRecentlyCompleted(
859
+ this.undisbursedQueue
860
+ )
861
+ }
862
+
863
+ this.logger.info(
864
+ `Got ${disbursable.length} undisbursed challenges${
865
+ this.undisbursedQueue.length !== disbursable.length
866
+ ? `, filtered out [${
867
+ disbursable.length - this.undisbursedQueue.length
868
+ }] challenges.`
869
+ : '.'
870
+ }`
871
+ )
872
+ return {}
873
+ }
874
+
875
+ /**
876
+ * Processes responses from `_performSingleAttestation`,
877
+ * bucketing errors into those that need retry and those that should be skipped.
878
+ *
879
+ * @memberof RewardsAttester
880
+ */
881
+ async _processResponses(
882
+ responses: AttestationResult[],
883
+ isFinalAttempt: boolean
884
+ ): Promise<{
885
+ successful: AttestationResult[]
886
+ noRetry: AttestationResult[]
887
+ needsRetry: AttestationResult[]
888
+ shouldReselect: boolean
889
+ failingNodes: string[]
890
+ }> {
891
+ const errors = SubmitAndEvaluateError
892
+ const AAO_ERRORS = new Set([
893
+ errors.HCAPTCHA,
894
+ errors.COGNITO_FLOW,
895
+ errors.AAO_ATTESTATION_REJECTION,
896
+ errors.AAO_ATTESTATION_UNKNOWN_RESPONSE
897
+ ])
898
+ // Account for errors from DN aggregation + Solana program
899
+ // CHALLENGE_INCOMPLETE and MISSING_CHALLENGES are already handled in the `submitAndEvaluate` flow -
900
+ // safe to assume those won't work if we see them at this point.
901
+ const NEEDS_RESELECT_ERRORS = new Set([
902
+ errors.INSUFFICIENT_DISCOVERY_NODE_COUNT,
903
+ errors.CHALLENGE_INCOMPLETE,
904
+ errors.MISSING_CHALLENGES
905
+ ])
906
+ const ALREADY_COMPLETE_ERRORS = new Set([
907
+ errors.ALREADY_DISBURSED,
908
+ errors.ALREADY_SENT
909
+ ])
910
+
911
+ const noRetry: AttestationResult[] = []
912
+ const successful: AttestationResult[] = []
913
+ // Filter our successful responses
914
+ const allErrors = responses.filter((res) => {
915
+ if (!res.error) {
916
+ successful.push(res)
917
+ this.reporter.reportSuccess({
918
+ userId: decodeHashId(res.userId) ?? -1,
919
+ challengeId: res.challengeId,
920
+ amount: res.amount,
921
+ specifier: res.specifier
922
+ })
923
+ return false
924
+ }
925
+ return true
926
+ }) as Array<AttestationResult & { error: string; phase: string }>
927
+
928
+ // Filter out responses that are already disbursed
929
+ const stillIncomplete = allErrors.filter(
930
+ ({ error }) => !ALREADY_COMPLETE_ERRORS.has(error)
931
+ )
932
+
933
+ // Filter to errors needing retry
934
+ const needsRetry = stillIncomplete.filter((res) => {
935
+ const report = {
936
+ userId: decodeHashId(res.userId) ?? -1,
937
+ challengeId: res.challengeId,
938
+ amount: res.amount,
939
+ error: res.error,
940
+ phase: res.phase,
941
+ specifier: res.specifier,
942
+ reason: 'unknown'
943
+ }
944
+
945
+ function getIsAAOError(err?: string): err is string {
946
+ return !!err && AAO_ERRORS.has(err)
947
+ }
948
+
949
+ const { error } = res
950
+ const isAAOError = getIsAAOError(error)
951
+ // Filter out and handle unretryable AAO errors
952
+ if (isAAOError) {
953
+ noRetry.push(res)
954
+ const errorType = {
955
+ [errors.HCAPTCHA]: 'hcaptcha',
956
+ [errors.COGNITO_FLOW]: 'cognito',
957
+ [errors.AAO_ATTESTATION_REJECTION]: 'rejection',
958
+ [errors.AAO_ATTESTATION_UNKNOWN_RESPONSE]: 'unknown'
959
+ // Some hacky typing here because we haen't typed the imported error type yet
960
+ }[error] as unknown as 'hcaptcha' | 'cognito' | 'rejection' | 'unknown'
961
+ report.reason = errorType
962
+ this.reporter.reportAAORejection(report)
963
+ } else if (isFinalAttempt) {
964
+ // Final attempt at retries,
965
+ // should be classified as noRetry
966
+ // and reported as a failure
967
+ noRetry.push(res)
968
+ this.reporter.reportFailure(report)
969
+ } else {
970
+ // Otherwise, retry it
971
+ this.reporter.reportRetry(report)
972
+ }
973
+ return !isAAOError && !isFinalAttempt
974
+ })
975
+
976
+ if (needsRetry.length) {
977
+ this.logger.info(
978
+ `Handling errors: ${JSON.stringify(
979
+ needsRetry.map(({ error, phase }) => ({ error, phase }))
980
+ )}`
981
+ )
982
+ }
983
+
984
+ // Reselect if necessary
985
+ const shouldReselect = needsRetry.some(({ error }) =>
986
+ NEEDS_RESELECT_ERRORS.has(error)
987
+ )
988
+
989
+ let failingNodes: string[] = []
990
+ if (shouldReselect) {
991
+ failingNodes = [
992
+ ...needsRetry.reduce((acc, cur) => {
993
+ if (cur.nodesToReselect) {
994
+ cur.nodesToReselect?.forEach((n) => acc.add(n))
995
+ }
996
+ return acc
997
+ }, new Set<string>())
998
+ ]
999
+ this.logger.info(`Failing nodes: ${JSON.stringify(failingNodes)}`)
1000
+ }
1001
+
1002
+ // Update state
1003
+ const now = Date.now()
1004
+ let update: {
1005
+ lastChallengeTime: number
1006
+ lastSuccessChallengeTime?: number
1007
+ } = {
1008
+ lastChallengeTime: now
1009
+ }
1010
+ if (successful.length) {
1011
+ update = {
1012
+ ...update,
1013
+ lastSuccessChallengeTime: now
1014
+ }
1015
+ }
1016
+ await this._updateState(update)
1017
+ return {
1018
+ successful,
1019
+ noRetry,
1020
+ needsRetry,
1021
+ shouldReselect,
1022
+ failingNodes
1023
+ }
1024
+ }
1025
+
1026
+ _disbursementToKey({ challengeId, userId, specifier }: Challenge) {
1027
+ return `${challengeId}_${userId}_${specifier}`
1028
+ }
1029
+
1030
+ async _backoff(retryCount: number) {
1031
+ const backoff = Math.min(
1032
+ this.cooldownMsec * Math.pow(this.backoffExponent, retryCount),
1033
+ this.maxCooldownMsec
1034
+ )
1035
+ this.logger.info(`Waiting [${backoff}] msec`)
1036
+ await this._updatePhase('RETRY_BACKOFF')
1037
+ return await this._delay(backoff)
1038
+ }
1039
+
1040
+ async _delay(waitTime: number): Promise<void> {
1041
+ return await new Promise((resolve) => setTimeout(resolve, waitTime))
1042
+ }
1043
+
1044
+ _addRecentlyDisbursed(challenges: Challenge[]) {
1045
+ const ids = challenges.map(this._disbursementToKey)
1046
+ this.recentlyDisbursedQueue.push(...ids)
1047
+ if (this.recentlyDisbursedQueue.length > MAX_DISBURSED_CACHE_SIZE) {
1048
+ this.recentlyDisbursedQueue.splice(
1049
+ 0,
1050
+ this.recentlyDisbursedQueue.length - MAX_DISBURSED_CACHE_SIZE
1051
+ )
1052
+ }
1053
+ }
1054
+
1055
+ async _filterRecentlyCompleted(challenges: Challenge[]) {
1056
+ const [poaThreshold, solanaThreshold] = await Promise.all([
1057
+ this.delayCalculator.getPOABlockThreshold(),
1058
+ this.delayCalculator.getSolanaSlotThreshold()
1059
+ ])
1060
+
1061
+ this.logger.info(
1062
+ `Filtering with POA threshold: ${poaThreshold}, Solana threshold: ${solanaThreshold}`
1063
+ )
1064
+ const res = challenges.filter(
1065
+ (c) =>
1066
+ c.completedBlocknumber <=
1067
+ (this.isSolanaChallenge(c.challengeId) ? solanaThreshold : poaThreshold)
1068
+ )
1069
+ if (res.length < challenges.length) {
1070
+ this.logger.info(
1071
+ `Filtered out ${challenges.length - res.length} recent challenges`
1072
+ )
1073
+ }
1074
+ return res
1075
+ }
1076
+
1077
+ async _updateState(newState: Partial<AttesterState>) {
1078
+ try {
1079
+ this.attesterState = {
1080
+ ...this.attesterState,
1081
+ ...newState,
1082
+ lastActionTime: Date.now()
1083
+ }
1084
+ await this._updateStateCallback(this.attesterState)
1085
+ } catch (e) {
1086
+ this.logger.error(`Got error updating state: ${e}`)
1087
+ }
1088
+ }
1089
+
1090
+ async _updatePhase(phase: ATTESTER_PHASE) {
1091
+ await this._updateState({ phase })
1092
+ }
1093
+ }