@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.
- package/.eslintrc +38 -0
- package/.prettierrc.js +1 -0
- package/.python-version +1 -0
- package/Dockerfile +15 -0
- package/README.md +3 -0
- package/babel.config.js +3 -0
- package/data-contracts/ABIs/AdminUpgradeabilityProxy.json +132 -0
- package/data-contracts/ABIs/BaseAdminUpgradeabilityProxy.json +113 -0
- package/data-contracts/ABIs/BaseUpgradeabilityProxy.json +22 -0
- package/data-contracts/ABIs/DiscoveryProviderFactory.json +189 -0
- package/data-contracts/ABIs/DiscoveryProviderFactoryInterface.json +61 -0
- package/data-contracts/ABIs/DiscoveryProviderStorage.json +205 -0
- package/data-contracts/ABIs/DiscoveryProviderStorageInterface.json +65 -0
- package/data-contracts/ABIs/ECDSA.json +4 -0
- package/data-contracts/ABIs/IPLDBlacklistFactory.json +168 -0
- package/data-contracts/ABIs/Initializable.json +4 -0
- package/data-contracts/ABIs/Migrations.json +67 -0
- package/data-contracts/ABIs/OpenZeppelinUpgradesAddress.json +4 -0
- package/data-contracts/ABIs/Ownable.json +79 -0
- package/data-contracts/ABIs/PlaylistFactory.json +669 -0
- package/data-contracts/ABIs/PlaylistFactoryInterface.json +42 -0
- package/data-contracts/ABIs/PlaylistStorage.json +250 -0
- package/data-contracts/ABIs/PlaylistStorageInterface.json +129 -0
- package/data-contracts/ABIs/Proxy.json +10 -0
- package/data-contracts/ABIs/Registry.json +240 -0
- package/data-contracts/ABIs/RegistryContract.json +102 -0
- package/data-contracts/ABIs/RegistryContractInterface.json +28 -0
- package/data-contracts/ABIs/RegistryInterface.json +66 -0
- package/data-contracts/ABIs/SigningLogic.json +43 -0
- package/data-contracts/ABIs/SigningLogicInitializable.json +46 -0
- package/data-contracts/ABIs/SocialFeatureFactory.json +460 -0
- package/data-contracts/ABIs/SocialFeatureStorage.json +225 -0
- package/data-contracts/ABIs/SocialFeatureStorageInterface.json +123 -0
- package/data-contracts/ABIs/TestContract.json +135 -0
- package/data-contracts/ABIs/TestContractInterface.json +19 -0
- package/data-contracts/ABIs/TestContractWithStorage.json +165 -0
- package/data-contracts/ABIs/TestContractWithStorageInterface.json +24 -0
- package/data-contracts/ABIs/TestStorage.json +144 -0
- package/data-contracts/ABIs/TestStorageInterface.json +42 -0
- package/data-contracts/ABIs/TestUserReplicaSetManager.json +432 -0
- package/data-contracts/ABIs/TrackFactory.json +391 -0
- package/data-contracts/ABIs/TrackFactoryInterface.json +73 -0
- package/data-contracts/ABIs/TrackStorage.json +223 -0
- package/data-contracts/ABIs/TrackStorageInterface.json +121 -0
- package/data-contracts/ABIs/UpgradeabilityProxy.json +37 -0
- package/data-contracts/ABIs/UserFactory.json +657 -0
- package/data-contracts/ABIs/UserFactoryInterface.json +65 -0
- package/data-contracts/ABIs/UserLibraryFactory.json +334 -0
- package/data-contracts/ABIs/UserReplicaSetManager.json +418 -0
- package/data-contracts/ABIs/UserStorage.json +233 -0
- package/data-contracts/ABIs/UserStorageInterface.json +93 -0
- package/data-contracts/signatureSchemas.ts +1236 -0
- package/dist/core.d.ts +446 -0
- package/dist/core.js +769 -0
- package/dist/core.js.map +1 -0
- package/dist/index.d.ts +689 -0
- package/dist/index.js +72850 -0
- package/dist/index.js.map +1 -0
- package/eth-contracts/ABIs/Address.json +4 -0
- package/eth-contracts/ABIs/AudiusAdminUpgradeabilityProxy.json +105 -0
- package/eth-contracts/ABIs/AudiusClaimDistributor.json +4968 -0
- package/eth-contracts/ABIs/AudiusToken.json +724 -0
- package/eth-contracts/ABIs/BaseUpgradeabilityProxy.json +23 -0
- package/eth-contracts/ABIs/Checkpointing.json +4 -0
- package/eth-contracts/ABIs/ClaimsManager.json +539 -0
- package/eth-contracts/ABIs/Context.json +11 -0
- package/eth-contracts/ABIs/DelegateManager.json +989 -0
- package/eth-contracts/ABIs/DelegateManagerV2.json +1049 -0
- package/eth-contracts/ABIs/DelegateManagerV2Bad.json +1049 -0
- package/eth-contracts/ABIs/ERC20.json +252 -0
- package/eth-contracts/ABIs/ERC20Burnable.json +287 -0
- package/eth-contracts/ABIs/ERC20Detailed.json +270 -0
- package/eth-contracts/ABIs/ERC20Mintable.json +364 -0
- package/eth-contracts/ABIs/ERC20Pausable.json +397 -0
- package/eth-contracts/ABIs/EthRewardsManager.json +174 -0
- package/eth-contracts/ABIs/Governance.json +938 -0
- package/eth-contracts/ABIs/GovernanceUpgraded.json +953 -0
- package/eth-contracts/ABIs/GovernanceV2.json +938 -0
- package/eth-contracts/ABIs/IERC20.json +200 -0
- package/eth-contracts/ABIs/Initializable.json +4 -0
- package/eth-contracts/ABIs/InitializableV2.json +14 -0
- package/eth-contracts/ABIs/Migrations.json +71 -0
- package/eth-contracts/ABIs/MinterRole.json +91 -0
- package/eth-contracts/ABIs/MockAccount.json +62 -0
- package/eth-contracts/ABIs/MockDelegateManager.json +55 -0
- package/eth-contracts/ABIs/MockStakingCaller.json +259 -0
- package/eth-contracts/ABIs/MockWormhole.json +106 -0
- package/eth-contracts/ABIs/OpenZeppelinUpgradesAddress.json +4 -0
- package/eth-contracts/ABIs/Ownable.json +93 -0
- package/eth-contracts/ABIs/Pausable.json +150 -0
- package/eth-contracts/ABIs/PauserRole.json +91 -0
- package/eth-contracts/ABIs/Proxy.json +10 -0
- package/eth-contracts/ABIs/Registry.json +288 -0
- package/eth-contracts/ABIs/Roles.json +4 -0
- package/eth-contracts/ABIs/SafeERC20.json +4 -0
- package/eth-contracts/ABIs/SafeMath.json +4 -0
- package/eth-contracts/ABIs/ServiceProviderFactory.json +1153 -0
- package/eth-contracts/ABIs/ServiceTypeManager.json +337 -0
- package/eth-contracts/ABIs/Staking.json +555 -0
- package/eth-contracts/ABIs/StakingUpgraded.json +570 -0
- package/eth-contracts/ABIs/TestContract.json +44 -0
- package/eth-contracts/ABIs/TrustedNotifierManager.json +265 -0
- package/eth-contracts/ABIs/Uint256Helpers.json +4 -0
- package/eth-contracts/ABIs/UpgradeabilityProxy.json +40 -0
- package/eth-contracts/ABIs/Wormhole.json +45 -0
- package/eth-contracts/ABIs/WormholeClient.json +155 -0
- package/examples/file.mp3 +0 -0
- package/examples/initAudiusLibs.js +86 -0
- package/examples/initializeVersions.js +95 -0
- package/examples/pic.jpg +0 -0
- package/initScripts/configureLocalDiscProv.js +167 -0
- package/initScripts/helpers/claim.js +43 -0
- package/initScripts/helpers/distributeTokens.js +24 -0
- package/initScripts/helpers/spRegistration.js +138 -0
- package/initScripts/helpers/utils.js +34 -0
- package/initScripts/helpers/version.js +93 -0
- package/initScripts/local.js +617 -0
- package/initScripts/mainnet.js +131 -0
- package/initScripts/manageProdRelayerWallets.js +191 -0
- package/package.json +125 -0
- package/rollup.config.js +164 -0
- package/scripts/AudiusClaimDistributor.json +4968 -0
- package/scripts/Wormhole.json +155 -0
- package/scripts/addCIDToIpldBlacklist.js +124 -0
- package/scripts/circleci-test.sh +53 -0
- package/scripts/communityRewards/transferCommunityRewardsToSolana.js +222 -0
- package/scripts/ipfs.sh +58 -0
- package/scripts/migrate_contracts.sh +25 -0
- package/scripts/reset.sh +65 -0
- package/scripts/test.sh +77 -0
- package/src/api/account.js +670 -0
- package/src/api/base.js +122 -0
- package/src/api/file.js +168 -0
- package/src/api/playlist.js +328 -0
- package/src/api/rewards.d.ts +4 -0
- package/src/api/rewards.js +682 -0
- package/src/api/serviceProvider.js +154 -0
- package/src/api/track.js +604 -0
- package/src/api/user.js +888 -0
- package/src/api/user.test.js +172 -0
- package/src/constants.ts +7 -0
- package/src/core.ts +3 -0
- package/src/index.js +6 -0
- package/src/libs.d.ts +3 -0
- package/src/libs.js +619 -0
- package/src/sanityChecks/addSecondaries.js +40 -0
- package/src/sanityChecks/assignReplicaSetIfNecessary.js +10 -0
- package/src/sanityChecks/index.d.ts +9 -0
- package/src/sanityChecks/index.js +31 -0
- package/src/sanityChecks/isCreator.js +73 -0
- package/src/sanityChecks/needsRecoveryEmail.js +20 -0
- package/src/sanityChecks/rolloverNodes.js +74 -0
- package/src/sanityChecks/sanitizeNodes.js +24 -0
- package/src/sanityChecks/syncNodes.js +28 -0
- package/src/sdk/constants.ts +10 -0
- package/src/sdk/index.ts +1 -0
- package/src/sdk/oauth/Oauth.ts +265 -0
- package/src/sdk/oauth/index.ts +1 -0
- package/src/sdk/sdk.ts +102 -0
- package/src/service-selection/ServiceSelection.test.ts +320 -0
- package/src/service-selection/ServiceSelection.ts +460 -0
- package/src/service-selection/constants.ts +14 -0
- package/src/service-selection/index.ts +1 -0
- package/src/services/ABIDecoder/AudiusABIDecoder.ts +71 -0
- package/src/services/ABIDecoder/index.ts +1 -0
- package/src/services/comstock/Comstock.ts +39 -0
- package/src/services/comstock/index.ts +1 -0
- package/src/services/contracts/ContractClient.ts +227 -0
- package/src/services/contracts/GovernedContractClient.ts +53 -0
- package/src/services/contracts/ProviderSelection.ts +42 -0
- package/src/services/creatorNode/CreatorNode.ts +1065 -0
- package/src/services/creatorNode/CreatorNodeSelection.test.ts +997 -0
- package/src/services/creatorNode/CreatorNodeSelection.ts +488 -0
- package/src/services/creatorNode/constants.ts +10 -0
- package/src/services/creatorNode/index.ts +2 -0
- package/src/services/dataContracts/AudiusContracts.ts +234 -0
- package/src/services/dataContracts/IPLDBlacklistFactoryClient.ts +73 -0
- package/src/services/dataContracts/PlaylistFactoryClient.ts +370 -0
- package/src/services/dataContracts/RegistryClient.ts +95 -0
- package/src/services/dataContracts/SocialFeatureFactoryClient.ts +196 -0
- package/src/services/dataContracts/TrackFactoryClient.ts +131 -0
- package/src/services/dataContracts/UserFactoryClient.ts +351 -0
- package/src/services/dataContracts/UserLibraryFactoryClient.ts +115 -0
- package/src/services/dataContracts/UserReplicaSetManagerClient.ts +206 -0
- package/src/services/dataContracts/index.ts +1 -0
- package/src/services/discoveryProvider/DiscoveryProvider.ts +1168 -0
- package/src/services/discoveryProvider/DiscoveryProviderSelection.test.ts +536 -0
- package/src/services/discoveryProvider/DiscoveryProviderSelection.ts +383 -0
- package/src/services/discoveryProvider/constants.ts +13 -0
- package/src/services/discoveryProvider/index.ts +1 -0
- package/src/services/discoveryProvider/requests.ts +629 -0
- package/src/services/ethContracts/AudiusTokenClient.ts +163 -0
- package/src/services/ethContracts/ClaimDistributionClient.ts +45 -0
- package/src/services/ethContracts/ClaimsManagerClient.ts +102 -0
- package/src/services/ethContracts/DelegateManagerClient.ts +480 -0
- package/src/services/ethContracts/EthContracts.ts +359 -0
- package/src/services/ethContracts/EthRewardsManagerClient.ts +33 -0
- package/src/services/ethContracts/GovernanceClient.ts +451 -0
- package/src/services/ethContracts/RegistryClient.ts +33 -0
- package/src/services/ethContracts/ServiceProviderFactoryClient.ts +691 -0
- package/src/services/ethContracts/ServiceTypeManagerClient.ts +112 -0
- package/src/services/ethContracts/StakingProxyClient.ts +97 -0
- package/src/services/ethContracts/TrustedNotifierManagerClient.ts +101 -0
- package/src/services/ethContracts/WormholeClient.ts +97 -0
- package/src/services/ethContracts/index.ts +1 -0
- package/src/services/ethWeb3Manager/EthWeb3Manager.ts +239 -0
- package/src/services/ethWeb3Manager/index.ts +1 -0
- package/src/services/hedgehog/Hedgehog.ts +96 -0
- package/src/services/hedgehog/index.ts +1 -0
- package/src/services/identity/IdentityService.ts +551 -0
- package/src/services/identity/index.ts +1 -0
- package/src/services/identity/requests.ts +65 -0
- package/src/services/schemaValidator/SchemaValidator.ts +105 -0
- package/src/services/schemaValidator/index.ts +1 -0
- package/src/services/schemaValidator/schemas/trackSchema.json +267 -0
- package/src/services/schemaValidator/schemas/userSchema.json +230 -0
- package/src/services/solanaAudiusData/errors.ts +20 -0
- package/src/services/solanaAudiusData/index.ts +1189 -0
- package/src/services/solanaWeb3Manager/errors.js +101 -0
- package/src/services/solanaWeb3Manager/index.d.ts +46 -0
- package/src/services/solanaWeb3Manager/index.js +655 -0
- package/src/services/solanaWeb3Manager/padBNToUint8Array.ts +7 -0
- package/src/services/solanaWeb3Manager/rewards.js +941 -0
- package/src/services/solanaWeb3Manager/rewardsAttester.ts +1093 -0
- package/src/services/solanaWeb3Manager/tokenAccount.js +149 -0
- package/src/services/solanaWeb3Manager/transactionHandler.js +345 -0
- package/src/services/solanaWeb3Manager/transfer.js +272 -0
- package/src/services/solanaWeb3Manager/userBank.js +160 -0
- package/src/services/solanaWeb3Manager/utils.d.ts +31 -0
- package/src/services/solanaWeb3Manager/utils.js +163 -0
- package/src/services/solanaWeb3Manager/wAudio.js +28 -0
- package/src/services/solanaWeb3Manager/wAudio.test.js +30 -0
- package/src/services/web3Manager/Web3Config.ts +14 -0
- package/src/services/web3Manager/Web3Manager.ts +360 -0
- package/src/services/web3Manager/XMLHttpRequest.ts +11 -0
- package/src/services/web3Manager/index.ts +2 -0
- package/src/services/wormhole/index.js +424 -0
- package/src/types.ts +8 -0
- package/src/userStateManager.ts +53 -0
- package/src/utils/apiSigning.ts +51 -0
- package/src/utils/captcha.ts +97 -0
- package/src/utils/estimateGas.ts +64 -0
- package/src/utils/fileHasher.ts +278 -0
- package/src/utils/importContractABI.d.ts +9 -0
- package/src/utils/importContractABI.js +19 -0
- package/src/utils/index.ts +11 -0
- package/src/utils/multiProvider.ts +72 -0
- package/src/utils/network.test.ts +127 -0
- package/src/utils/network.ts +308 -0
- package/src/utils/promiseFight.test.ts +87 -0
- package/src/utils/promiseFight.ts +36 -0
- package/src/utils/signatures.ts +139 -0
- package/src/utils/types.ts +34 -0
- package/src/utils/utils.test.ts +36 -0
- package/src/utils/utils.ts +235 -0
- package/src/utils/uuid.ts +14 -0
- package/src/web3.d.ts +9 -0
- package/src/web3.js +8 -0
- package/tests/assets/static_image.png +0 -0
- package/tests/assets/static_text.txt +1 -0
- package/tests/audiusTokenClientTest.js +37 -0
- package/tests/creatorNodeTest.js +19 -0
- package/tests/fileHasherTest.js +125 -0
- package/tests/governanceTest.js +382 -0
- package/tests/helpers.js +105 -0
- package/tests/index.js +14 -0
- package/tests/playlistClientTest.js +157 -0
- package/tests/providerSelectionTest.js +241 -0
- package/tests/registryClientTest.js +19 -0
- package/tests/rewardsAttesterTest.js +373 -0
- package/tests/serviceTypeManagerClientTest.js +33 -0
- package/tests/socialFeatureClientTest.js +79 -0
- package/tests/stakingTest.js +302 -0
- package/tests/trackClientTest.js +86 -0
- package/tests/userClientTest.js +121 -0
- package/tsconfig.json +10 -0
- package/types/@audius-hedgehog/index.d.ts +39 -0
- 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
|
+
}
|