@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,1065 @@
1
+ import axios, { AxiosError, AxiosRequestConfig } from 'axios'
2
+ import FormData from 'form-data'
3
+ import retry from 'async-retry'
4
+ import { Utils, uuid } from '../../utils'
5
+ import {
6
+ userSchemaType,
7
+ trackSchemaType,
8
+ Schemas
9
+ } from '../schemaValidator/SchemaValidator'
10
+ import type { Web3Manager } from '../web3Manager'
11
+ import type { CurrentUser, UserStateManager } from '../../userStateManager'
12
+
13
+ const { wait } = Utils
14
+
15
+ const MAX_TRACK_TRANSCODE_TIMEOUT = 3600000 // 1 hour
16
+ const POLL_STATUS_INTERVAL = 3000 // 3s
17
+ const BROWSER_SESSION_REFRESH_TIMEOUT = 604800000 // 1 week
18
+
19
+ type Metadata = {
20
+ track_segments: unknown
21
+ download?: {
22
+ is_downloadable: boolean
23
+ cid: string
24
+ }
25
+ cover_art_sizes: string
26
+ }
27
+
28
+ type ProgressCB = (loaded: number, total: number) => void
29
+
30
+ type MonitoringCallbacks = {
31
+ request?: Function
32
+ healthCheck?: Function
33
+ }
34
+
35
+ type ClockValueRequestConfig = {
36
+ user: CurrentUser
37
+ endpoint: string
38
+ timeout?: number
39
+ }
40
+
41
+ type FileUploadResponse = {
42
+ data: { uuid: string }
43
+ error: Error
44
+ }
45
+
46
+ // Currently only supports a single logged-in audius user
47
+ export class CreatorNode {
48
+ /* Static Utils */
49
+
50
+ /**
51
+ * Pulls off the primary creator node from a creator node endpoint string.
52
+ * @param endpoints user.creator_node_endpoint
53
+ */
54
+ static getPrimary(endpoints: string) {
55
+ return endpoints ? endpoints.split(',')[0] : ''
56
+ }
57
+
58
+ /**
59
+ * Pulls off the secondary creator nodes from a creator node endpoint string.
60
+ * @param endpoints user.creator_node_endpoint
61
+ */
62
+ static getSecondaries(endpoints: string) {
63
+ return endpoints ? endpoints.split(',').slice(1) : []
64
+ }
65
+
66
+ /**
67
+ * Pulls the user's creator nodes out of the list
68
+ * @param endpoints user.creator_node_endpoint
69
+ */
70
+ static getEndpoints(endpoints: string) {
71
+ return endpoints ? endpoints.split(',') : []
72
+ }
73
+
74
+ /**
75
+ * Builds the creator_node_endpoint value off of a primary and secondaries list
76
+ * @param primary the primary endpoint
77
+ * @param secondaries a list of secondary endpoints
78
+ */
79
+ static buildEndpoint(primary: string, secondaries: string[]) {
80
+ return [primary, ...secondaries].join()
81
+ }
82
+
83
+ /**
84
+ * Pulls off the user's clock value from a creator node endpoint and the user's wallet address.
85
+ * @param endpoint content node endpoint
86
+ * @param wallet user wallet address
87
+ * @param timeout max time alloted for clock request
88
+ * @param params optional query string params
89
+ */
90
+ static async getClockValue(
91
+ endpoint: string,
92
+ wallet: string,
93
+ timeout: number,
94
+ params: Record<string, string> = {}
95
+ ) {
96
+ const baseReq: AxiosRequestConfig = {
97
+ url: `/users/clock_status/${wallet}`,
98
+ method: 'get',
99
+ baseURL: endpoint
100
+ }
101
+
102
+ if (Object.keys(params).length > 0) {
103
+ baseReq.params = params
104
+ }
105
+
106
+ if (timeout) {
107
+ baseReq.timeout = timeout
108
+ }
109
+
110
+ try {
111
+ const { data: body } = await axios(baseReq)
112
+ return body.data.clockValue
113
+ } catch (err) {
114
+ throw new Error(
115
+ `Failed to get clock value for endpoint: ${endpoint} and wallet: ${wallet} with ${err}`
116
+ )
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Checks if a download is available from provided creator node endpoints
122
+ * @param endpoints creator node endpoints
123
+ * @param trackId
124
+ */
125
+ static async checkIfDownloadAvailable(endpoints: string, trackId: number) {
126
+ const primary = CreatorNode.getPrimary(endpoints)
127
+ if (primary) {
128
+ const req: AxiosRequestConfig = {
129
+ baseURL: primary,
130
+ url: `/tracks/download_status/${trackId}`,
131
+ method: 'get'
132
+ }
133
+ const { data: body } = await axios(req)
134
+ if (body.data.cid) return body.data.cid
135
+ }
136
+ // Download is not available, clients should display "processing"
137
+ return null
138
+ }
139
+
140
+ /* -------------- */
141
+
142
+ web3Manager: Web3Manager
143
+ creatorNodeEndpoint: string
144
+ isServer: boolean
145
+ userStateManager: UserStateManager
146
+ lazyConnect: boolean
147
+ schemas: Schemas
148
+ passList: Set<string> | null
149
+ blockList: Set<string> | null
150
+ monitoringCallbacks: MonitoringCallbacks
151
+ connected: boolean
152
+ connecting: boolean
153
+ authToken: null
154
+ maxBlockNumber: number
155
+
156
+ /**
157
+ * Constructs a service class for a creator node
158
+ * @param web3Manager
159
+ * @param creatorNodeEndpoint fallback creator node endpoint (to be deprecated)
160
+ * @param isServer
161
+ * @param userStateManager singleton UserStateManager instance
162
+ * @param lazyConnect whether or not to lazy connect (sign in) on load
163
+ * @param schemas
164
+ * @param passList whether or not to include only specified nodes (default null)
165
+ * @param blockList whether or not to exclude any nodes (default null)
166
+ * @param monitoringCallbacks callbacks to be invoked with metrics from requests sent to a service
167
+ */
168
+ constructor(
169
+ web3Manager: Web3Manager,
170
+ creatorNodeEndpoint: string,
171
+ isServer: boolean,
172
+ userStateManager: UserStateManager,
173
+ lazyConnect: boolean,
174
+ schemas: Schemas,
175
+ passList: Set<string> | null = null,
176
+ blockList: Set<string> | null = null,
177
+ monitoringCallbacks: MonitoringCallbacks = {}
178
+ ) {
179
+ this.web3Manager = web3Manager
180
+ // This is just 1 endpoint (primary), unlike the creator_node_endpoint field in user metadata
181
+ this.creatorNodeEndpoint = creatorNodeEndpoint
182
+ this.isServer = isServer
183
+ this.userStateManager = userStateManager
184
+ this.schemas = schemas
185
+
186
+ this.lazyConnect = lazyConnect
187
+ this.connected = false
188
+ this.connecting = false // a lock so multiple content node requests in parallel won't each try to auth
189
+ this.authToken = null
190
+ this.maxBlockNumber = 0
191
+
192
+ this.passList = passList
193
+ this.blockList = blockList
194
+ this.monitoringCallbacks = monitoringCallbacks
195
+ }
196
+
197
+ async init() {
198
+ if (!this.web3Manager) throw new Error('Failed to initialize CreatorNode')
199
+ if (!this.lazyConnect) {
200
+ await this.connect()
201
+ }
202
+ }
203
+
204
+ /** Establishes a connection to a content node endpoint */
205
+ async connect() {
206
+ this.connecting = true
207
+ await this._signupNodeUser(this.web3Manager.getWalletAddress())
208
+ await this._loginNodeUser()
209
+ this.connected = true
210
+ this.connecting = false
211
+ }
212
+
213
+ /** Checks if connected, otherwise establishing a connection */
214
+ async ensureConnected() {
215
+ if (!this.connected && !this.connecting) {
216
+ await this.connect()
217
+ } else if (this.connecting) {
218
+ let interval
219
+ // We were already connecting so wait for connection
220
+ await new Promise<void>((resolve) => {
221
+ interval = setInterval(() => {
222
+ if (this.connected) resolve()
223
+ }, 100)
224
+ })
225
+ clearInterval(interval)
226
+ }
227
+ }
228
+
229
+ getEndpoint() {
230
+ return this.creatorNodeEndpoint
231
+ }
232
+
233
+ /**
234
+ * Switch from one creatorNodeEndpoint to another including logging out from the old node, updating the endpoint and logging into new node */
235
+ async setEndpoint(creatorNodeEndpoint: string) {
236
+ // If the endpoints are the same, no-op.
237
+ if (this.creatorNodeEndpoint === creatorNodeEndpoint) return
238
+
239
+ if (this.connected) {
240
+ try {
241
+ await this._logoutNodeUser()
242
+ } catch (e: any) {
243
+ console.error(e.message)
244
+ }
245
+ }
246
+ this.connected = false
247
+ this.creatorNodeEndpoint = creatorNodeEndpoint
248
+ if (!this.lazyConnect) {
249
+ await this.connect()
250
+ }
251
+ }
252
+
253
+ /** Clear all connection state in this class by deleting authToken and setting 'connected' = false */
254
+ clearConnection() {
255
+ this.connected = false
256
+ this.authToken = null
257
+ }
258
+
259
+ /**
260
+ * Uploads creator content to a creator node
261
+ * @param metadata the creator metadata
262
+ */
263
+ async uploadCreatorContent(metadata: Metadata, blockNumber = null) {
264
+ // this does the actual validation before sending to the creator node
265
+ // if validation fails, validate() will throw an error
266
+ try {
267
+ this.schemas[userSchemaType].validate?.(metadata)
268
+
269
+ const requestObj: AxiosRequestConfig = {
270
+ url: '/audius_users/metadata',
271
+ method: 'post',
272
+ data: {
273
+ metadata,
274
+ blockNumber
275
+ }
276
+ }
277
+
278
+ const { data: body } = await this._makeRequest(requestObj)
279
+ return body
280
+ } catch (e) {
281
+ console.error('Error validating creator metadata', e)
282
+ }
283
+ }
284
+
285
+ /**
286
+ * Creates a creator on the creator node, associating user id with file content
287
+ * @param audiusUserId returned by user creation on-blockchain
288
+ * @param metadataFileUUID unique ID for metadata file
289
+ * @param blockNumber
290
+ */
291
+ async associateCreator(
292
+ audiusUserId: number,
293
+ metadataFileUUID: string,
294
+ blockNumber: number
295
+ ) {
296
+ this.maxBlockNumber = Math.max(this.maxBlockNumber, blockNumber)
297
+ await this._makeRequest({
298
+ url: '/audius_users',
299
+ method: 'post',
300
+ data: {
301
+ blockchainUserId: audiusUserId,
302
+ metadataFileUUID,
303
+ blockNumber: this.maxBlockNumber
304
+ }
305
+ })
306
+ }
307
+
308
+ /**
309
+ * Uploads a track (including audio and image content) to a creator node
310
+ * @param trackFile the audio content
311
+ * @param coverArtFile the image content
312
+ * @param metadata the metadata for the track
313
+ * @param onProgress an optional on progress callback
314
+ */
315
+ async uploadTrackContent(
316
+ trackFile: File,
317
+ coverArtFile: File,
318
+ metadata: Metadata,
319
+ onProgress: ProgressCB = () => {}
320
+ ) {
321
+ let loadedImageBytes = 0
322
+ let loadedTrackBytes = 0
323
+ let totalImageBytes = 0
324
+ let totalTrackBytes = 0
325
+ const onImageProgress: ProgressCB = (loaded, total) => {
326
+ loadedImageBytes = loaded
327
+ if (!totalImageBytes) totalImageBytes += total
328
+ if (totalImageBytes && totalTrackBytes) {
329
+ onProgress(
330
+ loadedImageBytes + loadedTrackBytes,
331
+ totalImageBytes + totalTrackBytes
332
+ )
333
+ }
334
+ }
335
+ const onTrackProgress: ProgressCB = (loaded, total) => {
336
+ loadedTrackBytes = loaded
337
+ if (!totalTrackBytes) totalTrackBytes += total
338
+ if ((!coverArtFile || totalImageBytes) && totalTrackBytes) {
339
+ onProgress(
340
+ loadedImageBytes + loadedTrackBytes,
341
+ totalImageBytes + totalTrackBytes
342
+ )
343
+ }
344
+ }
345
+
346
+ const uploadPromises = []
347
+ uploadPromises.push(this.uploadTrackAudio(trackFile, onTrackProgress))
348
+ if (coverArtFile)
349
+ uploadPromises.push(this.uploadImage(coverArtFile, true, onImageProgress))
350
+
351
+ const [trackContentResp, coverArtResp] = await Promise.all(uploadPromises)
352
+ metadata.track_segments = trackContentResp.track_segments
353
+ if (metadata.download?.is_downloadable) {
354
+ metadata.download.cid = trackContentResp.transcodedTrackCID
355
+ }
356
+
357
+ const sourceFile = trackContentResp.source_file
358
+ if (!sourceFile) {
359
+ throw new Error(
360
+ `Invalid or missing sourceFile in response: ${JSON.stringify(
361
+ trackContentResp
362
+ )}`
363
+ )
364
+ }
365
+
366
+ if (coverArtResp) {
367
+ metadata.cover_art_sizes = coverArtResp.dirCID
368
+ }
369
+ // Creates new track entity on creator node, making track's metadata available
370
+ // @returns {Object} {cid: CID of track metadata, id: id of track to be used with associate function}
371
+ const metadataResp = await this.uploadTrackMetadata(metadata, sourceFile)
372
+ return { ...metadataResp, ...trackContentResp }
373
+ }
374
+
375
+ /**
376
+ * Uploads track metadata to a creator node
377
+ * The metadata object must include a `track_id` field or a
378
+ * source file must be provided (returned from uploading track content).
379
+ * @param metadata
380
+ * @param sourceFile
381
+ */
382
+ async uploadTrackMetadata(metadata: Metadata, sourceFile: string) {
383
+ // this does the actual validation before sending to the creator node
384
+ // if validation fails, validate() will throw an error
385
+ try {
386
+ this.schemas[trackSchemaType].validate?.(metadata)
387
+ } catch (e) {
388
+ console.error('Error validating track metadata', e)
389
+ }
390
+
391
+ const { data: body } = await this._makeRequest(
392
+ {
393
+ url: '/tracks/metadata',
394
+ method: 'post',
395
+ data: {
396
+ metadata,
397
+ sourceFile
398
+ }
399
+ },
400
+ true
401
+ )
402
+ return body
403
+ }
404
+
405
+ /**
406
+ * Creates a track on the content node, associating track id with file content
407
+ * @param audiusTrackId returned by track creation on-blockchain
408
+ * @param metadataFileUUID unique ID for metadata file
409
+ * @param blockNumber
410
+ * @param transcodedTrackUUID the CID for the transcoded master if this is a first-time upload
411
+ */
412
+ async associateTrack(
413
+ audiusTrackId: number,
414
+ metadataFileUUID: string,
415
+ blockNumber: number,
416
+ transcodedTrackUUID: string
417
+ ) {
418
+ this.maxBlockNumber = Math.max(this.maxBlockNumber, blockNumber)
419
+ await this._makeRequest({
420
+ url: '/tracks',
421
+ method: 'post',
422
+ data: {
423
+ blockchainTrackId: audiusTrackId,
424
+ metadataFileUUID,
425
+ blockNumber: this.maxBlockNumber,
426
+ transcodedTrackUUID
427
+ }
428
+ })
429
+ }
430
+
431
+ /**
432
+ * Uploads an image to the connected content node
433
+ * @param file image to upload
434
+ * @param onProgress called with loaded bytes and total bytes
435
+ * @param timeoutMs timeout in ms axios request to upload file to CN will wait
436
+ * @return response body
437
+ */
438
+ async uploadImage(
439
+ file: File,
440
+ square = true,
441
+ onProgress: ProgressCB,
442
+ timeoutMs: number | null = null
443
+ ) {
444
+ const { data: body } = await this._uploadFile(
445
+ file,
446
+ '/image_upload',
447
+ onProgress,
448
+ { square },
449
+ /* retries */ undefined,
450
+ timeoutMs
451
+ )
452
+ return body
453
+ }
454
+
455
+ /**
456
+ * @param file track to upload
457
+ * @param onProgress called with loaded bytes and total bytes
458
+ * @return response body
459
+ */
460
+ async uploadTrackAudio(file: File, onProgress: ProgressCB) {
461
+ return await this.handleAsyncTrackUpload(file, onProgress)
462
+ }
463
+
464
+ async handleAsyncTrackUpload(file: File, onProgress: ProgressCB) {
465
+ const {
466
+ data: { uuid }
467
+ } = await this._uploadFile(file, '/track_content_async', onProgress)
468
+ return await this.pollProcessingStatus(uuid)
469
+ }
470
+
471
+ async pollProcessingStatus(uuid: string) {
472
+ const route = this.creatorNodeEndpoint + '/async_processing_status'
473
+ const start = Date.now()
474
+ while (Date.now() - start < MAX_TRACK_TRANSCODE_TIMEOUT) {
475
+ try {
476
+ const { status, resp } = await this.getTrackContentProcessingStatus(
477
+ uuid
478
+ )
479
+ // Should have a body structure of:
480
+ // { transcodedTrackCID, transcodedTrackUUID, track_segments, source_file }
481
+ if (status && status === 'DONE') return resp
482
+ if (status && status === 'FAILED') {
483
+ await this._handleErrorHelper(
484
+ new Error(
485
+ `Track content async upload failed: uuid=${uuid}, error=${resp}`
486
+ ),
487
+ route,
488
+ uuid
489
+ )
490
+ }
491
+ } catch (e) {
492
+ // Catch errors here and swallow them. Errors don't signify that the track
493
+ // upload has failed, just that we were unable to establish a connection to the node.
494
+ // This allows polling to retry
495
+ console.error(`Failed to poll for processing status, ${e}`)
496
+ }
497
+
498
+ await wait(POLL_STATUS_INTERVAL)
499
+ }
500
+
501
+ // TODO: update MAX_TRACK_TRANSCODE_TIMEOUT if generalizing this method
502
+ await this._handleErrorHelper(
503
+ new Error(
504
+ `Track content async upload took over ${MAX_TRACK_TRANSCODE_TIMEOUT}ms. uuid=${uuid}`
505
+ ),
506
+ route,
507
+ uuid
508
+ )
509
+ }
510
+
511
+ /**
512
+ * Gets the task progress given the task type and uuid associated with the task
513
+ * @param uuid the uuid of the track transcoding task
514
+ * @returns the status, and the success or failed response if the task is complete
515
+ */
516
+ async getTrackContentProcessingStatus(uuid: string) {
517
+ const { data: body } = await this._makeRequest({
518
+ url: '/async_processing_status',
519
+ params: {
520
+ uuid
521
+ },
522
+ method: 'get'
523
+ })
524
+
525
+ return body
526
+ }
527
+
528
+ /**
529
+ * Given a particular endpoint to a creator node, check whether
530
+ * this user has a sync in progress on that node.
531
+ * @param endpoint
532
+ * @param timeout ms
533
+ */
534
+ async getSyncStatus(endpoint: string, timeout: number | null = null) {
535
+ const user = this.userStateManager.getCurrentUser()
536
+ if (user) {
537
+ const req: AxiosRequestConfig = {
538
+ baseURL: endpoint,
539
+ url: `/sync_status/${user.wallet}`,
540
+ method: 'get'
541
+ }
542
+ if (timeout) req.timeout = timeout
543
+ const { data: body } = await axios(req)
544
+ const status = body.data
545
+ return {
546
+ status,
547
+ userBlockNumber: user.blocknumber,
548
+ trackBlockNumber: user.track_blocknumber,
549
+ // Whether or not the endpoint is behind in syncing
550
+ isBehind:
551
+ status.latestBlockNumber <
552
+ Math.max(user.blocknumber, user.track_blocknumber),
553
+ isConfigured: status.latestBlockNumber !== -1
554
+ }
555
+ }
556
+ throw new Error('No current user')
557
+ }
558
+
559
+ /**
560
+ * Syncs a secondary creator node for a given user
561
+ * @param secondary
562
+ * @param primary specific primary to use
563
+ * @param immediate whether or not this is a blocking request and handled right away
564
+ * @param validate whether or not to validate the provided secondary is valid
565
+ */
566
+ async syncSecondary(
567
+ secondary: string,
568
+ primary?: string,
569
+ immediate = false,
570
+ validate = true
571
+ ) {
572
+ const user = this.userStateManager.getCurrentUser()
573
+ if (!user) return
574
+
575
+ if (!primary) {
576
+ primary = CreatorNode.getPrimary(user.creator_node_endpoint)
577
+ }
578
+ const secondaries = new Set(
579
+ CreatorNode.getSecondaries(user.creator_node_endpoint)
580
+ )
581
+ if (primary && secondary && (!validate || secondaries.has(secondary))) {
582
+ const req: AxiosRequestConfig = {
583
+ baseURL: secondary,
584
+ url: '/sync',
585
+ method: 'post',
586
+ data: {
587
+ wallet: [user.wallet],
588
+ creator_node_endpoint: primary,
589
+ immediate
590
+ }
591
+ }
592
+ return await axios(req)
593
+ }
594
+ return undefined
595
+ }
596
+
597
+ /* ------- INTERNAL FUNCTIONS ------- */
598
+
599
+ /**
600
+ * Signs up a creator node user with a wallet address
601
+ * @param walletAddress
602
+ */
603
+ async _signupNodeUser(walletAddress: string) {
604
+ await this._makeRequest(
605
+ {
606
+ url: '/users',
607
+ method: 'post',
608
+ data: { walletAddress }
609
+ },
610
+ false
611
+ )
612
+ }
613
+
614
+ /**
615
+ * Logs user into cnode, if not already logged in.
616
+ * Requests a challenge from cnode, sends signed challenge response to cn.
617
+ * If successful, receive and set authToken locally.
618
+ */
619
+ async _loginNodeUser() {
620
+ if (this.authToken) {
621
+ return
622
+ }
623
+
624
+ const walletPublicKey = this.web3Manager.getWalletAddress()
625
+ let clientChallengeKey
626
+ let url: string | undefined
627
+
628
+ try {
629
+ const challengeResp = await this._makeRequest(
630
+ {
631
+ url: '/users/login/challenge',
632
+ method: 'get',
633
+ params: {
634
+ walletPublicKey
635
+ }
636
+ },
637
+ false
638
+ )
639
+
640
+ clientChallengeKey = challengeResp.data.challenge
641
+ url = '/users/login/challenge'
642
+ } catch (e) {
643
+ const requestUrl = this.creatorNodeEndpoint + '/users/login/challenge'
644
+ await this._handleErrorHelper(e as Error, requestUrl)
645
+ }
646
+
647
+ const signature = await this.web3Manager.sign(clientChallengeKey)
648
+
649
+ if (url) {
650
+ const resp = await this._makeRequest(
651
+ {
652
+ url,
653
+ method: 'post',
654
+ data: {
655
+ data: clientChallengeKey,
656
+ signature
657
+ }
658
+ },
659
+ false
660
+ )
661
+ this.authToken = resp.data.sessionToken
662
+ }
663
+
664
+ setTimeout(() => {
665
+ this.clearConnection()
666
+ }, BROWSER_SESSION_REFRESH_TIMEOUT)
667
+ }
668
+
669
+ /** Calls logout on the content node. Needs an authToken for this since logout is an authenticated endpoint */
670
+ async _logoutNodeUser() {
671
+ if (!this.authToken) {
672
+ return
673
+ }
674
+ await this._makeRequest(
675
+ {
676
+ url: '/users/logout',
677
+ method: 'post'
678
+ },
679
+ false
680
+ )
681
+ this.authToken = null
682
+ }
683
+
684
+ /**
685
+ * Gets and returns the clock values across the replica set for the wallet in userStateManager.
686
+ * @returns Array of objects with the structure:
687
+ *
688
+ * {
689
+ * type: 'primary' or 'secondary',
690
+ * endpoint: <Content Node endpoint>,
691
+ * clockValue: clock value (should be an integer) or null
692
+ * }
693
+ *
694
+ * 'clockValue' may be null if the request to fetch the clock value fails
695
+ */
696
+ async getClockValuesFromReplicaSet() {
697
+ const user = this.userStateManager.getCurrentUser()
698
+ if (!user || !user.creator_node_endpoint) {
699
+ console.error('No user or Content Node endpoint found')
700
+ return
701
+ }
702
+
703
+ const replicaSet = CreatorNode.getEndpoints(user.creator_node_endpoint)
704
+ const clockValueResponses = await Promise.all(
705
+ replicaSet.map(
706
+ async (endpoint) => await this._clockValueRequest({ user, endpoint })
707
+ )
708
+ )
709
+
710
+ return clockValueResponses
711
+ }
712
+
713
+ /**
714
+ * Wrapper around getClockValue() to return either a proper or null clock value
715
+ * @param {Object} param
716
+ * @param {Object} param.user user metadata object from userStateManager
717
+ * @param {string} param.endpoint the Content Node endpoint to check the clock value for
718
+ * @param {number?} [param.timeout=1000] the max time allotted for a clock request; defaulted to 1000ms
719
+ */
720
+ async _clockValueRequest({
721
+ user,
722
+ endpoint,
723
+ timeout = 1000
724
+ }: ClockValueRequestConfig) {
725
+ const primary = CreatorNode.getPrimary(user.creator_node_endpoint)
726
+ const type = primary === endpoint ? 'primary' : 'secondary'
727
+
728
+ try {
729
+ const clockValue = await CreatorNode.getClockValue(
730
+ endpoint,
731
+ user.wallet,
732
+ timeout
733
+ )
734
+ return {
735
+ type,
736
+ endpoint,
737
+ clockValue
738
+ }
739
+ } catch (e) {
740
+ console.error(
741
+ `Error in getting clock status for ${user.wallet} at ${endpoint}: ${e}`
742
+ )
743
+ return {
744
+ type,
745
+ endpoint,
746
+ clockValue: null
747
+ }
748
+ }
749
+ }
750
+
751
+ /**
752
+ * Makes an axios request to the connected creator node.
753
+ * @param requiresConnection if set, the currently configured creator node
754
+ * is connected to before the request is made.
755
+ * @return response body
756
+ */
757
+ async _makeRequest(
758
+ axiosRequestObj: AxiosRequestConfig,
759
+ requiresConnection = true
760
+ ) {
761
+ const work = async () => {
762
+ if (requiresConnection) {
763
+ await this.ensureConnected()
764
+ }
765
+
766
+ axiosRequestObj.headers = axiosRequestObj.headers || {}
767
+
768
+ if (this.authToken) {
769
+ axiosRequestObj.headers['X-Session-ID'] = this.authToken
770
+ }
771
+
772
+ const user = this.userStateManager.getCurrentUser()
773
+ if (user?.wallet && user.user_id) {
774
+ axiosRequestObj.headers['User-Wallet-Addr'] = user.wallet
775
+ axiosRequestObj.headers['User-Id'] = user.user_id
776
+ }
777
+
778
+ const requestId = uuid()
779
+ axiosRequestObj.headers['X-Request-ID'] = requestId
780
+
781
+ axiosRequestObj.baseURL = this.creatorNodeEndpoint
782
+
783
+ // Axios throws for non-200 responses
784
+ const url = new URL(`${axiosRequestObj.baseURL}${axiosRequestObj.url}`)
785
+ const start = Date.now()
786
+ try {
787
+ const resp = await axios(axiosRequestObj)
788
+ const duration = Date.now() - start
789
+
790
+ if (this.monitoringCallbacks.request) {
791
+ try {
792
+ this.monitoringCallbacks.request({
793
+ endpoint: url.origin,
794
+ pathname: url.pathname,
795
+ queryString: url.search,
796
+ signer: resp.data.signer,
797
+ signature: resp.data.signature,
798
+ requestMethod: axiosRequestObj.method,
799
+ status: resp.status,
800
+ responseTimeMillis: duration
801
+ })
802
+ } catch (e) {
803
+ // Swallow errors -- this method should not throw generally
804
+ console.error(e)
805
+ }
806
+ }
807
+ // Axios `data` field gets the response body
808
+ return resp.data
809
+ } catch (e) {
810
+ const error = e as AxiosError
811
+ const resp = error.response
812
+ const duration = Date.now() - start
813
+
814
+ if (this.monitoringCallbacks.request) {
815
+ try {
816
+ this.monitoringCallbacks.request({
817
+ endpoint: url.origin,
818
+ pathname: url.pathname,
819
+ queryString: url.search,
820
+ requestMethod: axiosRequestObj.method,
821
+ status: resp?.status,
822
+ responseTimeMillis: duration
823
+ })
824
+ } catch (e) {
825
+ // Swallow errors -- this method should not throw generally
826
+ console.error(e)
827
+ }
828
+ }
829
+
830
+ // if the content node returns an invalid auth token error, clear connection and reconnect
831
+ if (resp?.data?.error?.includes('Invalid authentication token')) {
832
+ this.clearConnection()
833
+ try {
834
+ await this.ensureConnected()
835
+ } catch (e) {
836
+ console.error((e as Error).message)
837
+ }
838
+ }
839
+
840
+ await this._handleErrorHelper(error, axiosRequestObj.url, requestId)
841
+ }
842
+ }
843
+ return await retry(
844
+ async () => {
845
+ return await work()
846
+ },
847
+ {
848
+ // Retry function 3x
849
+ // 1st retry delay = 500ms, 2nd = 1500ms, 3rd...nth retry = 4000 ms (capped)
850
+ minTimeout: 500,
851
+ maxTimeout: 4000,
852
+ factor: 3,
853
+ retries: 3,
854
+ onRetry: (err) => {
855
+ if (err) {
856
+ console.log('makeRequest retry error: ', err)
857
+ }
858
+ }
859
+ }
860
+ )
861
+ }
862
+
863
+ /**
864
+ * Create headers and formData for file upload
865
+ * @param file the file to upload
866
+ * @returns headers and formData in an object
867
+ */
868
+ createFormDataAndUploadHeaders(
869
+ file: File,
870
+ extraFormDataOptions: Record<string, unknown> = {}
871
+ ) {
872
+ // form data is from browser, not imported npm module
873
+ const formData = new FormData()
874
+ formData.append('file', file)
875
+ Object.keys(extraFormDataOptions).forEach((key) => {
876
+ formData.append(key, `${extraFormDataOptions[key]}`)
877
+ })
878
+
879
+ let headers: Record<string, string | null> = {}
880
+ if (this.isServer) {
881
+ headers = formData.getHeaders()
882
+ }
883
+ headers['X-Session-ID'] = this.authToken
884
+
885
+ const requestId = uuid()
886
+ headers['X-Request-ID'] = requestId
887
+
888
+ const user = this.userStateManager.getCurrentUser()
889
+ if (user?.wallet && user.user_id) {
890
+ // TODO change to X-User-Wallet-Address and X-User-Id per convention
891
+ headers['User-Wallet-Addr'] = user.wallet
892
+ headers['User-Id'] = user.user_id
893
+ }
894
+
895
+ return { headers, formData }
896
+ }
897
+
898
+ /**
899
+ * Uploads a file to the connected creator node.
900
+ * @param file
901
+ * @param route route to handle upload (image_upload, track_upload, etc.)
902
+ * @param onProgress called with loaded bytes and total bytes
903
+ * @param extraFormDataOptions extra FormData fields passed to the upload
904
+ * @param retries max number of attempts made for axios request to upload file to CN before erroring
905
+ * @param timeoutMs timeout in ms axios request to upload file to CN will wait
906
+ */
907
+ async _uploadFile(
908
+ file: File,
909
+ route: string,
910
+ onProgress: ProgressCB = () => {},
911
+ extraFormDataOptions: Record<string, unknown> = {},
912
+ retries = 2,
913
+ timeoutMs: number | null = null
914
+ // @ts-expect-error re-throwing at the end of this function breaks exisiting impl
915
+ ): Promise<FileUploadResponse> {
916
+ await this.ensureConnected()
917
+
918
+ const { headers, formData } = this.createFormDataAndUploadHeaders(
919
+ file,
920
+ extraFormDataOptions
921
+ )
922
+ const requestId = headers['X-Request-ID']
923
+
924
+ let total: number
925
+ const url = this.creatorNodeEndpoint + route
926
+
927
+ try {
928
+ // Hack alert!
929
+ //
930
+ // Axios auto-detects browser vs node based on
931
+ // the existance of XMLHttpRequest at the global namespace, which
932
+ // is imported by a web3 module, causing Axios to incorrectly
933
+ // presume we're in a browser env when we're in a node env.
934
+ // For uploads to work in a node env,
935
+ // axios needs to correctly detect we're in node and use the `http` module
936
+ // rather than XMLHttpRequest. We force that here.
937
+ // https://github.com/axios/axios/issues/1180
938
+
939
+ const isBrowser = typeof window !== 'undefined'
940
+
941
+ console.debug(`Uploading file to ${url}`)
942
+
943
+ const reqParams: AxiosRequestConfig = {
944
+ headers: headers,
945
+ adapter: isBrowser
946
+ ? require('axios/lib/adapters/xhr')
947
+ : require('axios/lib/adapters/http'),
948
+ // Add a 10% inherit processing time for the file upload.
949
+ onUploadProgress: (progressEvent: {
950
+ total: number
951
+ loaded: number
952
+ }) => {
953
+ if (!total) total = progressEvent.total
954
+ console.info(`Upload in progress: ${progressEvent.loaded} / ${total}`)
955
+ onProgress(progressEvent.loaded, total)
956
+ },
957
+ // Set content length headers (only applicable in server/node environments).
958
+ // See: https://github.com/axios/axios/issues/1362
959
+ maxContentLength: Infinity,
960
+ // @ts-expect-error TODO: including even though it's not an axios config. should double check
961
+ maxBodyLength: Infinity
962
+ }
963
+
964
+ if (timeoutMs) {
965
+ reqParams.timeout = timeoutMs
966
+ }
967
+
968
+ const resp = await axios.post<FileUploadResponse>(
969
+ url,
970
+ formData,
971
+ reqParams
972
+ )
973
+
974
+ if (resp.data?.error) {
975
+ throw new Error(JSON.stringify(resp.data.error))
976
+ }
977
+
978
+ // @ts-expect-error total should be set in `onUploadProgress` which runs before `onProgress` is called
979
+ onProgress(total, total)
980
+ return resp.data
981
+ } catch (e: any) {
982
+ const error = e as AxiosError
983
+ if (!error.response && retries > 0) {
984
+ console.warn(
985
+ `Network Error in request ${requestId} with ${retries} retries... retrying`
986
+ )
987
+ console.warn(error)
988
+ // eslint-disable-next-line @typescript-eslint/return-await -- possible issue with return await
989
+ return this._uploadFile(
990
+ file,
991
+ route,
992
+ onProgress,
993
+ extraFormDataOptions,
994
+ retries - 1
995
+ )
996
+ } else if (
997
+ error.response?.data?.error?.includes('Invalid authentication token')
998
+ ) {
999
+ // if the content node returns an invalid auth token error, clear connection and reconnect
1000
+ this.clearConnection()
1001
+ try {
1002
+ await this.ensureConnected()
1003
+ } catch (e: any) {
1004
+ console.error(e.message)
1005
+ }
1006
+ }
1007
+
1008
+ await this._handleErrorHelper(error, url, requestId)
1009
+ }
1010
+ }
1011
+
1012
+ async _handleErrorHelper(
1013
+ e: Error | AxiosError,
1014
+ requestUrl?: string,
1015
+ requestId: string | null = null
1016
+ ) {
1017
+ if ('response' in e && e.response?.data?.error) {
1018
+ const cnRequestID = e.response.headers['cn-request-id']
1019
+ // cnRequestID will be the same as requestId if it receives the X-Request-ID header
1020
+ const errMessage = `Server returned error: [${e.response.status.toString()}] [${
1021
+ e.response.data.error
1022
+ }] for request: [${cnRequestID}, ${requestId}]`
1023
+
1024
+ console.error(errMessage)
1025
+ throw new Error(errMessage)
1026
+ } else if (!('response' in e)) {
1027
+ // delete headers, may contain tokens
1028
+ if ('config' in e && e.config.headers) delete e.config.headers
1029
+
1030
+ const errorMsg = `Network error while making request ${requestId} to ${requestUrl}:\nStringified Error:${JSON.stringify(
1031
+ e
1032
+ )}\n`
1033
+ console.error(errorMsg, e)
1034
+
1035
+ try {
1036
+ const newRequestId = uuid()
1037
+ const endpoint = `${this.creatorNodeEndpoint}/health_check`
1038
+ const res = await axios(endpoint, {
1039
+ headers: {
1040
+ 'X-Request-ID': newRequestId
1041
+ }
1042
+ })
1043
+ console.log(
1044
+ `Successful health check for ${requestId}: ${JSON.stringify(
1045
+ res.data
1046
+ )}`
1047
+ )
1048
+ } catch (e) {
1049
+ console.error(
1050
+ `Failed health check immediately after network error ${requestId}`,
1051
+ e
1052
+ )
1053
+ }
1054
+
1055
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string -- TODO
1056
+ throw new Error(`${errorMsg}${e}`)
1057
+ } else {
1058
+ const errorMsg = `Unknown error while making request ${requestId} to ${requestUrl}:\nStringified Error:${JSON.stringify(
1059
+ e
1060
+ )}\n`
1061
+ console.error(errorMsg, e)
1062
+ throw e
1063
+ }
1064
+ }
1065
+ }