diff --git a/.changeset/spicy-elephants-shout.md b/.changeset/spicy-elephants-shout.md new file mode 100644 index 00000000000..06b00faf2c0 --- /dev/null +++ b/.changeset/spicy-elephants-shout.md @@ -0,0 +1,6 @@ +--- +'@audius/sdk': minor +'@audius/spl': major +--- + +Update to use PaymentRouterProgram in @audius/spl and enable track purchase in SDK diff --git a/.circleci/src/jobs/test-audius-cmd.yml b/.circleci/src/jobs/test-audius-cmd.yml index 8a6852b771c..a6de09a4864 100644 --- a/.circleci/src/jobs/test-audius-cmd.yml +++ b/.circleci/src/jobs/test-audius-cmd.yml @@ -14,8 +14,7 @@ steps: cd ~/audius-protocol/packages/libs . ~/.profile audius-compose connect --nopass - # Disabled until it can be fixed - # ./scripts/check-generated-types.sh + ./scripts/check-generated-types.sh - run: name: cleanup no_output_timeout: 5m diff --git a/dev-tools/config.json b/dev-tools/config.json index 34bfec8de76..b8425df1838 100644 --- a/dev-tools/config.json +++ b/dev-tools/config.json @@ -138,7 +138,7 @@ "DDEX_KEY": "49d5e13d355709b615b7cce7369174fb240b6b39", "DDEX_SECRET": "2b2c2b90d9a489234ae629a5284de84fb0633306257f17667aaebf2345d92152", "SESSION_SECRET": "something random", - "DDEX_ADMIN_ALLOWLIST": "127559427", + "DDEX_ADMIN_ALLOWLIST": "127559427,555164012", "NODE_ENV": "stage" } } diff --git a/docs/docs/node-operator/setup/installation.mdx b/docs/docs/node-operator/setup/installation.mdx index c399d5b6559..2fac0d962a7 100644 --- a/docs/docs/node-operator/setup/installation.mdx +++ b/docs/docs/node-operator/setup/installation.mdx @@ -60,8 +60,10 @@ During installation there will be prompts for required environment variables. Th Installing and managing Audius Nodes is (in most cases) a 3 step process. 1. Install `audius-ctl` -2. Edit the configuration file -3. Run your Audius Nodes +2. Confirm your + [ssh access and port configuration](/node-operator/setup/hardware-requirements#system-configuration) +3. Edit the configuration file +4. Run your Audius Nodes --- @@ -76,7 +78,13 @@ Run the following command to install the controller utility, `audius-ctl` curl -sSL https://install.audius.org | sh ``` -> checkout the [code on GitHub](https://github.com/AudiusProject/audius-d) +:::tip Where to install audius-ctl + +While it is recommended to install the controller utility on a separate computer, such as your +laptop, any machine can operate as a Controller. Check the +[Advanced Usage page](/node-operator/setup/advanced#audius-control-utility) for more information. + +::: --- @@ -179,6 +187,8 @@ Flags: Use "audius-ctl [command] --help" for more information about a command. ``` +> checkout the [code on GitHub](https://github.com/AudiusProject/audius-d) + --- ## Migration Guide diff --git a/mediorum/.version.json b/mediorum/.version.json index 78cfcac85f6..e8ef112d3f9 100644 --- a/mediorum/.version.json +++ b/mediorum/.version.json @@ -1,4 +1,4 @@ { - "version": "0.6.97", + "version": "0.6.98", "service": "content-node" } diff --git a/packages/commands/src/purchase-content.mjs b/packages/commands/src/purchase-content.mjs index d9c2b750ee9..c40a3eee6db 100644 --- a/packages/commands/src/purchase-content.mjs +++ b/packages/commands/src/purchase-content.mjs @@ -1,7 +1,8 @@ import chalk from 'chalk' import { program } from 'commander' -import { initializeAudiusLibs } from './utils.mjs' +import { initializeAudiusLibs, initializeAudiusSdk } from './utils.mjs' +import { Utils } from '@audius/sdk' program .command('purchase-content') @@ -79,3 +80,39 @@ program process.exit(0) }) + + +program.command('purchase-track') + .description('Buys a track using USDC') + .argument('', 'The track ID') + .option('-f, --from [from]', 'The account purchasing the content (handle)') + .option( + '-e, --extra-amount [amount]', + 'Extra amount to pay in addition to the price (in dollars)' + , parseFloat) + .action(async (id, { from, extraAmount }) => { + const audiusLibs = await initializeAudiusLibs(from) + const userIdNumber = audiusLibs.userStateManager.getCurrentUserId() + const userId = Utils.encodeHashId(userIdNumber) + const trackId = Utils.encodeHashId(id) + + // extract privkey and pubkey from hedgehog + // only works with accounts created via audius-cmd + const wallet = audiusLibs?.hedgehog?.getWallet() + const privKey = wallet?.getPrivateKeyString() + const pubKey = wallet?.getAddressString() + + // init sdk with priv and pub keys as api keys and secret + // this enables writes via sdk + const audiusSdk = await initializeAudiusSdk({ apiKey: pubKey, apiSecret: privKey }) + + try { + console.log('Purchasing track...', { trackId, userId, extraAmount }) + const response = await audiusSdk.tracks.purchase({ trackId, userId, extraAmount }) + console.log(chalk.green('Successfully purchased track')) + console.log(chalk.yellow('Transaction Signature:'), response) + } catch (err) { + program.error(err) + } + process.exit(0) + }) diff --git a/packages/commands/src/route-tokens-to-user-bank.mjs b/packages/commands/src/route-tokens-to-user-bank.mjs index 9de066ff419..3246db61224 100644 --- a/packages/commands/src/route-tokens-to-user-bank.mjs +++ b/packages/commands/src/route-tokens-to-user-bank.mjs @@ -16,7 +16,7 @@ import { import chalk from 'chalk' import { Option, program } from 'commander' -import { route } from '@audius/spl' +import { PaymentRouterProgram } from '@audius/spl' import { initializeAudiusLibs } from './utils.mjs' @@ -189,16 +189,16 @@ program TOKEN_DECIMALS[mint] ) - const paymentRouterInstruction = await route( - paymentRouterTokenAccount, - paymentRouterPda, + const paymentRouterInstruction = await PaymentRouterProgram.route({ + sender: paymentRouterTokenAccount, + senderOwner: paymentRouterPda, paymentRouterPdaBump, - [userbankPublicKey], // recipients - [amount], - amount, - TOKEN_PROGRAM_ID, - paymentRouterPublicKey - ) + recipients: [userbankPublicKey], // recipients + amounts: [amount], + totalAmount: amount, + tokenProgramId: TOKEN_PROGRAM_ID, + programId: paymentRouterPublicKey + }) transferTx.add(transferInstruction, paymentRouterInstruction) diff --git a/packages/commands/src/utils.mjs b/packages/commands/src/utils.mjs index ff2dad89309..dc9e5c90c41 100644 --- a/packages/commands/src/utils.mjs +++ b/packages/commands/src/utils.mjs @@ -2,9 +2,9 @@ import { Utils as AudiusUtils, sdk as AudiusSdk, libs as AudiusLibs, - developmentConfig, DiscoveryNodeSelector, - EntityManager + SolanaRelay, + Configuration, } from "@audius/sdk"; import { PublicKey } from "@solana/web3.js"; @@ -75,30 +75,34 @@ export const initializeAudiusLibs = async (handle) => { let audiusSdk; export const initializeAudiusSdk = async ({ apiKey = undefined, apiSecret = undefined } = {}) => { - const discoveryNodeSelector = new DiscoveryNodeSelector({ - healthCheckThresholds: { - minVersion: developmentConfig.minVersion, - maxBlockDiff: developmentConfig.maxBlockDiff, - maxSlotDiffPlays: developmentConfig.maxSlotDiffPlays, - }, - bootstrapServices: developmentConfig.discoveryNodes, - }) - const entityManager = new EntityManager({ - discoveryNodeSelector, - web3ProviderUrl: developmentConfig.web3ProviderUrl, - contractAddress: developmentConfig.entityManagerContractAddress, - identityServiceUrl: developmentConfig.identityServiceUrl, - useDiscoveryRelay: true, - }) + + const solanaRelay = new SolanaRelay( + new Configuration({ + basePath: '/solana', + headers: { + 'Content-Type': 'application/json' + }, + middleware: [ + { + pre: async (context) => { + const endpoint = 'http://audius-protocol-discovery-provider-1' + const url = `${endpoint}${context.url}` + return { url, init: context.init } + } + } + ] + }) + ) + if (!audiusSdk) { audiusSdk = AudiusSdk({ appName: "audius-cmd", apiKey, apiSecret, + environment: 'development', services: { - discoveryNodeSelector, - entityManager - }, + solanaRelay + } }); } diff --git a/packages/common/src/schemas/upload/uploadFormSchema.ts b/packages/common/src/schemas/upload/uploadFormSchema.ts index 50b66ebfc2e..bd389fcb132 100644 --- a/packages/common/src/schemas/upload/uploadFormSchema.ts +++ b/packages/common/src/schemas/upload/uploadFormSchema.ts @@ -267,9 +267,8 @@ export const createCollectionSchema = (collectionType: 'playlist' | 'album') => mood: MoodSchema, tags: z.optional(z.string()) }), - is_unlisted: z.optional(z.boolean()), - is_album: z.literal(collectionType === 'album'), is_private: z.optional(z.boolean()), + is_album: z.literal(collectionType === 'album'), tracks: z.array(z.object({ metadata: CollectionTrackMetadataSchema })), ddex_release_ids: z.optional(z.record(z.string()).nullable()), artists: z.optional(z.array(DDEXResourceContributor).nullable()), diff --git a/packages/common/src/services/audius-backend/solana.ts b/packages/common/src/services/audius-backend/solana.ts index 3018ba20a16..cda306181d2 100644 --- a/packages/common/src/services/audius-backend/solana.ts +++ b/packages/common/src/services/audius-backend/solana.ts @@ -423,7 +423,7 @@ export const purchaseContentWithPaymentRouter = async ( purchaseAccess }: PurchaseContentWithPaymentRouterArgs ) => { - const solanaWeb3Manager = (await audiusBackendInstance.getAudiusLibs()) + const solanaWeb3Manager = (await audiusBackendInstance.getAudiusLibsTyped()) .solanaWeb3Manager! const tx = await solanaWeb3Manager.purchaseContentWithPaymentRouter({ id, @@ -436,7 +436,7 @@ export const purchaseContentWithPaymentRouter = async ( skipSendAndReturnTransaction: true, purchaseAccess }) - return tx + return tx as Transaction } export const findAssociatedTokenAddress = async ( diff --git a/packages/common/src/store/pages/audio-rewards/slice.ts b/packages/common/src/store/pages/audio-rewards/slice.ts index e0fd45a996b..76e18d9fa79 100644 --- a/packages/common/src/store/pages/audio-rewards/slice.ts +++ b/packages/common/src/store/pages/audio-rewards/slice.ts @@ -119,6 +119,17 @@ const slice = createSlice({ is_disbursed: true } } + const newlyDisbursedAmount = specifiers.reduce( + (acc, val) => acc + val.amount, + 0 + ) + const previouslyDisbursedAmount = + state.userChallengesOverrides[challengeId]?.disbursed_amount ?? + userChallenge.disbursed_amount + state.userChallengesOverrides[challengeId] = { + ...state.userChallengesOverrides[challengeId], + disbursed_amount: previouslyDisbursedAmount + newlyDisbursedAmount + } } else { state.userChallengesOverrides[challengeId] = { ...state.userChallengesOverrides[challengeId], diff --git a/packages/ddex/ingester/README.md b/packages/ddex/ingester/README.md index 7c997c4a1b5..db9cbcf62d4 100644 --- a/packages/ddex/ingester/README.md +++ b/packages/ddex/ingester/README.md @@ -5,7 +5,7 @@ Crawls and parses new DDEX uploads. ### Local Dev 1. Make sure the DDEX dependencies are running: `audius-compose up --ddex-deps` 2. (Optional) See the webapp README to start that server and go through the OAuth flow with a staging user -3. Parse a file: `IS_DEV=true AWS_ENDPOINT=http://ingress:4566 DDEX_CHOREOGRAPHY=ERNBatched go run cmd/main.go ./e2e_test/fixtures/batch/fuga/20240305090456555 --wipe` +3. Parse a file: `IS_DEV=true AWS_ENDPOINT=http://ingress:4566 DDEX_CHOREOGRAPHY=ERNBatched go run cmd/main.go ./e2e_test/fixtures/batch/fuga/20240305090206405 --wipe` 4. Alternatively, run `IS_DEV=true AWS_ENDPOINT=http://ingress:4566 DDEX_CHOREOGRAPHY=ERNBatched air` to run the server with hot reloading. Then, run the webapp (see its README) to use its "Re-process All" button to test code changes against files you sync using the below instructions @@ -19,4 +19,4 @@ Usage: go run ./cmd/sync s3://ddex-prod--raw/20240305090456555 aws s3 ls s3://ddex-prod--raw/20240305090456555 --profile local -``` \ No newline at end of file +``` diff --git a/packages/ddex/ingester/parser/ern38x.go b/packages/ddex/ingester/parser/ern38x.go index f6358136dd9..034ce52addd 100644 --- a/packages/ddex/ingester/parser/ern38x.go +++ b/packages/ddex/ingester/parser/ern38x.go @@ -1144,7 +1144,6 @@ func processSoundRecordingNode(sNode *xmlquery.Node) (recording *SoundRecording, name := safeInnerText(contributorNode.SelectElement("PartyName/FullName")) seqNo, seqNoErr := strconv.Atoi(contributorNode.SelectAttr("SequenceNumber")) if seqNoErr != nil { - fmt.Printf("error parsing ResourceContributor %s's SequenceNumber: %v\n", name, seqNoErr) seqNo = -1 } contributor := common.ResourceContributor{ @@ -1168,7 +1167,6 @@ func processSoundRecordingNode(sNode *xmlquery.Node) (recording *SoundRecording, name := safeInnerText(indirectContributorNode.SelectElement("PartyName/FullName")) seqNo, seqNoErr := strconv.Atoi(indirectContributorNode.SelectAttr("SequenceNumber")) if seqNoErr != nil { - fmt.Printf("error parsing IndirectResourceContributor %s's SequenceNumber: %v\n", name, seqNoErr) seqNo = -1 } contributor := common.ResourceContributor{ diff --git a/packages/ddex/processor/cli.ts b/packages/ddex/processor/cli.ts index 073e25fe03e..c0a2c2ae897 100644 --- a/packages/ddex/processor/cli.ts +++ b/packages/ddex/processor/cli.ts @@ -7,12 +7,12 @@ import { pollS3 } from './src/s3poller' import { deleteRelease, publishValidPendingReleases, - sdkService, } from './src/publishRelease' import { sync } from './src/s3sync' import { startServer } from './src/server' import { sleep } from './src/util' import { releaseRepo } from './src/db' +import { createSdkService } from './src/sdk' program .name('ddexer') @@ -45,7 +45,7 @@ program .action(async (id) => { // find release and delete it const release = releaseRepo.get(id) - const sdk = (await sdkService).getSdk() + const sdk = (await createSdkService()).getSdk() await deleteRelease(sdk, release!) }) diff --git a/packages/ddex/processor/src/publishRelease.ts b/packages/ddex/processor/src/publishRelease.ts index 13138520f52..d25de903942 100644 --- a/packages/ddex/processor/src/publishRelease.ts +++ b/packages/ddex/processor/src/publishRelease.ts @@ -14,15 +14,13 @@ import { DDEXContributor, DDEXRelease, DDEXResource } from './parseDelivery' import { readAssetWithCaching } from './s3poller' import { createSdkService } from './sdk' -export const sdkService = createSdkService() - export async function publishValidPendingReleases(opts?: { republish: boolean }) { const rows = releaseRepo.all({ pendingPublish: true }) if (!rows.length) return - const sdk = (await sdkService).getSdk() + const sdk = (await createSdkService()).getSdk() for (const row of rows) { const parsed = row._parsed! diff --git a/packages/ddex/processor/src/server.ts b/packages/ddex/processor/src/server.ts index 82c370724cf..3ab5b1a6971 100644 --- a/packages/ddex/processor/src/server.ts +++ b/packages/ddex/processor/src/server.ts @@ -18,6 +18,7 @@ import { import { prepareAlbumMetadata, prepareTrackMetadatas } from './publishRelease' import { readAssetWithCaching } from './s3poller' import { parseBool } from './util' +import { startUsersPoller } from './usersPoller' const { NODE_ENV, DDEX_KEY, DDEX_URL, COOKIE_SECRET } = process.env const COOKIE_NAME = 'audiusUser' @@ -43,7 +44,7 @@ app.get('/', async (c) => { : ''} ${me ? html` -

Welcome back ${me.name}

+

Welcome back @${me.handle}

log out ` : html` login `} @@ -547,4 +548,6 @@ export function startServer() { fetch: app.fetch, port, }) + + startUsersPoller().catch(console.error) } diff --git a/packages/ddex/processor/src/usersPoller.ts b/packages/ddex/processor/src/usersPoller.ts new file mode 100644 index 00000000000..9c7093a680e --- /dev/null +++ b/packages/ddex/processor/src/usersPoller.ts @@ -0,0 +1,29 @@ +import { userRepo } from './db' +import { createSdkService } from './sdk' + +export async function startUsersPoller() { + const sdk = (await createSdkService()).getSdk() + + // Periodic task to fetch user data and update names + setInterval(async () => { + try { + const users = userRepo.all() + + for (const user of users) { + const { data: userResponse } = await sdk.users.getUser({ id: user.id }) + if (!userResponse) { + throw new Error(`Error fetching user ${user.id} from sdk`) + } + if (userResponse.name !== user.name) { + userRepo.upsert({ + ...user, + name: userResponse.name + }) + console.log(`Updated user ${user.id}'s name`) + } + } + } catch (error) { + console.error('Failed to update user names:', error) + } + }, 300000) // Runs every 5 min +} diff --git a/packages/discovery-provider/.version.json b/packages/discovery-provider/.version.json index 5da6e1a1434..fbae57b6fa4 100644 --- a/packages/discovery-provider/.version.json +++ b/packages/discovery-provider/.version.json @@ -1,4 +1,4 @@ { - "version": "0.6.97", + "version": "0.6.98", "service": "discovery-node" } diff --git a/packages/discovery-provider/plugins/pedalboard/apps/solana-relay/src/routes/relay/assertRelayAllowedInstructions.ts b/packages/discovery-provider/plugins/pedalboard/apps/solana-relay/src/routes/relay/assertRelayAllowedInstructions.ts index be841f7967f..fdd20a15059 100644 --- a/packages/discovery-provider/plugins/pedalboard/apps/solana-relay/src/routes/relay/assertRelayAllowedInstructions.ts +++ b/packages/discovery-provider/plugins/pedalboard/apps/solana-relay/src/routes/relay/assertRelayAllowedInstructions.ts @@ -5,13 +5,15 @@ import { decodeInstruction, isCloseAccountInstruction, isTransferCheckedInstruction, - isSyncNativeInstruction + isSyncNativeInstruction, + getAssociatedTokenAddressSync } from '@solana/spl-token' import { PublicKey, TransactionInstruction, SystemProgram, - SystemInstruction + SystemInstruction, + ComputeBudgetProgram } from '@solana/web3.js' import { InvalidRelayInstructionError } from './InvalidRelayInstructionError' @@ -31,6 +33,7 @@ const MEMO_V2_PROGRAM_ID = 'MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr' const CLAIMABLE_TOKEN_PROGRAM_ID = config.claimableTokenProgramId const REWARDS_MANAGER_PROGRAM_ID = config.rewardsManagerProgramId const TRACK_LISTEN_COUNT_PROGRAM_ID = config.trackListenCountProgramId +const PAYMENT_ROUTER_PROGRAM_ID = config.paymentRouterProgramId const JUPITER_AGGREGATOR_V6_PROGRAM_ID = 'JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4' @@ -64,6 +67,17 @@ const deriveUserBank = async ( ) } +const PAYMENT_ROUTER_WALLET = PublicKey.findProgramAddressSync( + [Buffer.from('payment_router')], + new PublicKey(PAYMENT_ROUTER_PROGRAM_ID) +)[0] + +const PAYMENT_ROUTER_USDC_TOKEN_ACCOUNT = getAssociatedTokenAddressSync( + new PublicKey(usdcMintAddress), + PAYMENT_ROUTER_WALLET, + true +) + /** * Only allow the createTokenAccount instruction of the Associated Token * Account program, provided it has matching close instructions. @@ -89,6 +103,14 @@ const assertAllowedAssociatedTokenAccountProgramInstruction = ( ) } + // Allow creating associated tokens for the Payment Router + if ( + decodedInstruction.keys.owner.pubkey.toBase58() === + PAYMENT_ROUTER_WALLET.toBase58() + ) { + return + } + // Protect against feePayer drain by ensuring that there's always as // many account close instructions as creates const matchingCreateInstructions = instructions @@ -154,7 +176,12 @@ const assertAllowedTokenProgramInstruction = async ( wallet, claimableTokenAuthorities['usdc'] ) - if (!destination.equals(userbank)) { + + // Check that destination is either a userbank or a payment router token account + if ( + !destination.equals(userbank) && + !destination.equals(PAYMENT_ROUTER_USDC_TOKEN_ACCOUNT) + ) { throw new InvalidRelayInstructionError( instructionIndex, `Invalid destination account: ${destination.toBase58()}` @@ -405,9 +432,11 @@ export const assertRelayAllowedInstructions = async ( case Secp256k1Program.programId.toBase58(): assertValidSecp256k1ProgramInstruction(i, instruction) break + case PAYMENT_ROUTER_PROGRAM_ID: case MEMO_PROGRAM_ID: case MEMO_V2_PROGRAM_ID: case TRACK_LISTEN_COUNT_PROGRAM_ID: + case ComputeBudgetProgram.programId.toBase58(): // All instructions of these programs are allowed break default: diff --git a/packages/discovery-provider/src/api/v1/models/access_gate.py b/packages/discovery-provider/src/api/v1/models/access_gate.py index 333c838ad87..829c7313de1 100644 --- a/packages/discovery-provider/src/api/v1/models/access_gate.py +++ b/packages/discovery-provider/src/api/v1/models/access_gate.py @@ -3,8 +3,24 @@ from .common import ns from .extensions.models import OneOfModel, WildcardModel -tip_gate = ns.model("tip_gate", {"tip_user_id": fields.Integer(required=True)}) -follow_gate = ns.model("follow_gate", {"follow_user_id": fields.Integer(required=True)}) +tip_gate = ns.model( + "tip_gate", + { + "tip_user_id": fields.Integer( + required=True, + description="Must tip the given user ID to unlock", + ) + }, +) +follow_gate = ns.model( + "follow_gate", + { + "follow_user_id": fields.Integer( + required=True, + description="Must follow the given user ID to unlock", + ) + }, +) nft_collection = ns.model( "nft_collection", { @@ -16,7 +32,14 @@ }, ) nft_gate = ns.model( - "nft_gate", {"nft_collection": fields.Nested(nft_collection, required=True)} + "nft_gate", + { + "nft_collection": fields.Nested( + nft_collection, + required=True, + description="Must hold an NFT of the given collection to unlock", + ) + }, ) wild_card_split = WildcardModel( @@ -32,7 +55,14 @@ }, ) purchase_gate = ns.model( - "purchase_gate", {"usdc_purchase": fields.Nested(usdc_gate, required=True)} + "purchase_gate", + { + "usdc_purchase": fields.Nested( + usdc_gate, + required=True, + description="Must pay the total price and split to the given addresses to unlock", + ) + }, ) access_gate = ns.add_model( diff --git a/packages/discovery-provider/src/api/v1/models/tracks.py b/packages/discovery-provider/src/api/v1/models/tracks.py index 3bd02c4ae90..47ce8c5f042 100644 --- a/packages/discovery-provider/src/api/v1/models/tracks.py +++ b/packages/discovery-provider/src/api/v1/models/tracks.py @@ -72,7 +72,13 @@ track = ns.model( "Track", { + "access": fields.Nested( + access, description="Describes what access the given user has" + ), "artwork": fields.Nested(track_artwork, allow_null=True), + "blocknumber": fields.Integer( + required=True, description="The blocknumber this track was last updated" + ), "description": fields.String, "genre": fields.String, "id": fields.String(required=True), @@ -86,7 +92,7 @@ "orig_filename": fields.String( allow_null=True ), # remove nullability after backfill - "is_original_available": fields.Boolean, + "is_original_available": fields.Boolean(), "mood": fields.String, "release_date": fields.String, "remix_of": fields.Nested(remix_parent), @@ -103,6 +109,20 @@ "is_streamable": fields.Boolean, "ddex_app": fields.String(allow_null=True), "playlists_containing_track": fields.List(fields.Integer), + "is_stream_gated": fields.Boolean( + description="Whether or not the owner has restricted streaming behind an access gate" + ), + "stream_conditions": NestedOneOf( + access_gate, + allow_null=True, + description="How to unlock stream access to the track", + ), + "is_download_gated": fields.Boolean( + description="Whether or not the owner has restricted downloading behind an access gate" + ), + "download_conditions": NestedOneOf( + access_gate, allow_null=True, description="How to unlock the track download" + ), }, ) @@ -133,7 +153,6 @@ "track_full", track, { - "blocknumber": fields.Integer(required=True), "create_date": fields.String, "cover_art_sizes": fields.String, "cover_art_cids": fields.Nested(cover_art, allow_null=True), @@ -161,11 +180,6 @@ "cover_art": fields.String, "remix_of": fields.Nested(full_remix_parent), "is_available": fields.Boolean, - "is_stream_gated": fields.Boolean, - "stream_conditions": NestedOneOf(access_gate, allow_null=True), - "is_download_gated": fields.Boolean, - "download_conditions": NestedOneOf(access_gate, allow_null=True), - "access": fields.Nested(access), "ai_attribution_user_id": fields.Integer(allow_null=True), "audio_upload_id": fields.String, "preview_start_seconds": fields.Float, diff --git a/packages/discovery-provider/src/api/v1/models/users.py b/packages/discovery-provider/src/api/v1/models/users.py index a37e38d5b66..4efe0dffe9a 100644 --- a/packages/discovery-provider/src/api/v1/models/users.py +++ b/packages/discovery-provider/src/api/v1/models/users.py @@ -49,6 +49,10 @@ "supporter_count": fields.Integer(required=True), "supporting_count": fields.Integer(required=True), "total_audio_balance": fields.Integer(required=True), + "wallet": fields.String( + required=True, + description="The user's Ethereum wallet address for their account", + ), }, ) @@ -62,7 +66,6 @@ "waudio_balance": fields.String(required=True), "associated_sol_wallets_balance": fields.String(required=True), "blocknumber": fields.Integer(required=True), - "wallet": fields.String(required=True), "created_at": fields.String(required=True), "is_storage_v2": fields.Boolean(required=True), "creator_node_endpoint": fields.String, diff --git a/packages/discovery-provider/src/api/v1/tracks.py b/packages/discovery-provider/src/api/v1/tracks.py index 7d7fa9b9480..abb73db4964 100644 --- a/packages/discovery-provider/src/api/v1/tracks.py +++ b/packages/discovery-provider/src/api/v1/tracks.py @@ -150,7 +150,7 @@ class Track(Resource): @cache(ttl_sec=5) def get(self, track_id): decoded_id = decode_with_abort(track_id, ns) - return get_single_track(decoded_id, None, ns) + return get_single_track(decoded_id, None, ns, exclude_gated=False) @full_ns.route(TRACK_ROUTE) diff --git a/packages/discovery-provider/src/queries/get_developer_apps.py b/packages/discovery-provider/src/queries/get_developer_apps.py index bfede47858e..d337ad35d49 100644 --- a/packages/discovery-provider/src/queries/get_developer_apps.py +++ b/packages/discovery-provider/src/queries/get_developer_apps.py @@ -101,6 +101,7 @@ def get_developer_apps_with_grant_for_user(user_id: int) -> List[Dict]: DeveloperApp.address, DeveloperApp.name, DeveloperApp.description, + DeveloperApp.image_url, Grant.user_id.label("grantor_user_id"), Grant.created_at.label("grant_created_at"), Grant.updated_at.label("grant_updated_at"), @@ -121,9 +122,10 @@ def get_developer_apps_with_grant_for_user(user_id: int) -> List[Dict]: "address": row[0], "name": row[1], "description": row[2], - "grantor_user_id": row[3], - "grant_created_at": row[4], - "grant_updated_at": row[5], + "image_url": row[3], + "grantor_user_id": row[4], + "grant_created_at": row[5], + "grant_updated_at": row[6], } for row in rows ] diff --git a/packages/discovery-provider/src/solana/solana_helpers.py b/packages/discovery-provider/src/solana/solana_helpers.py index bf6d02a463a..d83427cd5dd 100644 --- a/packages/discovery-provider/src/solana/solana_helpers.py +++ b/packages/discovery-provider/src/solana/solana_helpers.py @@ -39,5 +39,8 @@ def get_derived_address(base, hashed_eth_pk, spl_token_id): # Static Memo Program ID MEMO_PROGRAM_ID = "Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo" +# Static Memo Program ID +MEMO_V2_PROGRAM_ID = "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr" + # Static Jupiter Swap Program ID JUPITER_PROGRAM_ID = "JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4" diff --git a/packages/discovery-provider/src/tasks/entity_manager/entities/playlist.py b/packages/discovery-provider/src/tasks/entity_manager/entities/playlist.py index ea3a42fb01b..58c79ba7444 100644 --- a/packages/discovery-provider/src/tasks/entity_manager/entities/playlist.py +++ b/packages/discovery-provider/src/tasks/entity_manager/entities/playlist.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import datetime, timezone from typing import Union from sqlalchemy import desc @@ -23,6 +23,7 @@ copy_record, is_ddex_signer, validate_signer, + parse_release_date, ) from src.tasks.metadata import immutable_playlist_fields from src.tasks.task_helpers import generate_slug_and_collision_id @@ -312,6 +313,12 @@ def validate_playlist_tx(params: ManageEntityParameters): raise IndexingValidationError( f"Cannot create playlist {playlist_id} below the offset" ) + if is_ddex_signer(params.signer): + parsed_release_date = parse_release_date(params.metadata["release_date"]) + if parsed_release_date and parsed_release_date > datetime.now().astimezone(timezone.utc): + raise IndexingValidationError( + f"Cannot create playlist {playlist_id} with a future relaese date" + ) else: if playlist_id not in params.existing_records["Playlist"]: raise IndexingValidationError( @@ -437,8 +444,13 @@ def create_playlist(params: ManageEntityParameters): last_added_to = None ddex_app = None + created_at = params.block_datetime if is_ddex_signer(params.signer): ddex_app = params.signer + if params.action == Action.CREATE: + parsed_release_date = parse_release_date(params.metadata["release_date"]) + if parsed_release_date: + created_at = str(parsed_release_date) # type: ignore for track in tracks: if "track" not in track or "time" not in track: @@ -471,7 +483,7 @@ def create_playlist(params: ManageEntityParameters): is_stream_gated=params.metadata.get("is_stream_gated", False), stream_conditions=params.metadata.get("stream_conditions", None), playlist_contents={"track_ids": tracks_with_index_time}, - created_at=params.block_datetime, + created_at=created_at, updated_at=params.block_datetime, blocknumber=params.block_number, blockhash=params.event_blockhash, @@ -667,6 +679,9 @@ def process_playlist_data_event( playlist_record.updated_at = block_datetime playlist_record.metadata_multihash = metadata_cid + if is_ddex_signer(params.signer): + playlist_record.ddex_app = params.signer + params.logger.info( f"playlist.py | EntityManager | Updated playlist record {playlist_record}" ) diff --git a/packages/discovery-provider/src/tasks/entity_manager/entities/track.py b/packages/discovery-provider/src/tasks/entity_manager/entities/track.py index 9022b551773..ad6b270c7aa 100644 --- a/packages/discovery-provider/src/tasks/entity_manager/entities/track.py +++ b/packages/discovery-provider/src/tasks/entity_manager/entities/track.py @@ -29,6 +29,7 @@ copy_record, is_ddex_signer, validate_signer, + parse_release_date, ) from src.tasks.metadata import immutable_track_fields from src.tasks.task_helpers import generate_slug_and_collision_id @@ -229,33 +230,6 @@ def is_valid_json_field(metadata, field): return False -def parse_release_date(release_date_str): - # try various time formats - if not release_date_str: - return None - - try: - return datetime.strptime( - release_date_str, "%a %b %d %Y %H:%M:%S GMT%z" - ).astimezone(timezone.utc) - except ValueError: - pass - - try: - return datetime.strptime(release_date_str, "%Y-%m-%dT%H:%M:%S.%fZ").astimezone( - timezone.utc - ) - except ValueError: - pass - - try: - return datetime.fromtimestamp(int(release_date_str)).astimezone(timezone.utc) - except (ValueError, TypeError): - pass - - return None - - def populate_track_record_metadata(track_record: Track, track_metadata, handle, action): # Iterate over the track_record keys # Update track_record values for which keys exist in track_metadata @@ -510,6 +484,9 @@ def update_track_record( ): populate_track_record_metadata(track, metadata, handle, params.action) + if is_ddex_signer(params.signer): + track.ddex_app = params.signer + # if cover_art CID is of a dir, store under _sizes field instead if track.cover_art: track.cover_art_sizes = track.cover_art diff --git a/packages/discovery-provider/src/tasks/entity_manager/utils.py b/packages/discovery-provider/src/tasks/entity_manager/utils.py index 5b5bf038f79..1db4b788975 100644 --- a/packages/discovery-provider/src/tasks/entity_manager/utils.py +++ b/packages/discovery-provider/src/tasks/entity_manager/utils.py @@ -1,6 +1,6 @@ import json import os -from datetime import datetime +from datetime import datetime, timezone from enum import Enum from typing import Dict, List, Literal, Set, Tuple, TypedDict, Union @@ -512,3 +512,30 @@ def is_ddex_signer(signer): address.lower() for address in ddex_apps.split(",") ) return False + + +def parse_release_date(release_date_str): + # try various time formats + if not release_date_str: + return None + + try: + return datetime.strptime( + release_date_str, "%a %b %d %Y %H:%M:%S GMT%z" + ).astimezone(timezone.utc) + except ValueError: + pass + + try: + return datetime.strptime(release_date_str, "%Y-%m-%dT%H:%M:%S.%fZ").astimezone( + timezone.utc + ) + except ValueError: + pass + + try: + return datetime.fromtimestamp(int(release_date_str)).astimezone(timezone.utc) + except (ValueError, TypeError): + pass + + return None diff --git a/packages/discovery-provider/src/tasks/index_payment_router.py b/packages/discovery-provider/src/tasks/index_payment_router.py index 15ec191725b..b82aeb5dafe 100644 --- a/packages/discovery-provider/src/tasks/index_payment_router.py +++ b/packages/discovery-provider/src/tasks/index_payment_router.py @@ -59,6 +59,7 @@ has_log, ) from src.utils.prometheus_metric import save_duration_metric +from src.utils.redis_cache import get_solana_transaction_key from src.utils.redis_constants import ( latest_sol_payment_router_db_tx_key, latest_sol_payment_router_program_tx_key, @@ -156,10 +157,15 @@ def check_config(): def get_sol_tx_info( - solana_client_manager: SolanaClientManager, - tx_sig: str, + solana_client_manager: SolanaClientManager, tx_sig: str, redis: Redis ): try: + existing_tx = redis.get(get_solana_transaction_key(tx_sig)) + if existing_tx is not None and existing_tx != "": + logger.info(f"index_payment_router.py | Cache hit: {tx_sig}") + tx_info = GetTransactionResp.from_json(existing_tx.decode("utf-8")) + return (tx_info, tx_sig) + logger.info(f"index_payment_router.py | Cache miss: {tx_sig}") tx_info = solana_client_manager.get_sol_tx_info(tx_sig) return (tx_info, tx_sig) except SolanaTransactionFetchError: @@ -259,7 +265,7 @@ def parse_route_transaction_memo( # TODO: Wait for blocknumber to be indexed by ACDC logger.debug( - f"index_payment_router.py | Found content_metadata in memo: type={type}, id={id}, blocknumber={blocknumber} user_id={purchaser_user_id}" + f"index_payment_router.py | Found content_metadata in memo: type={type}, id={id}, blocknumber={blocknumber} user_id={purchaser_user_id} access={access}" ) price = None splits = None @@ -679,6 +685,7 @@ def process_route_instruction( f"index_payment_router.py | tx: {tx_sig} | $AUDIO payment router transactions are not yet indexed. Skipping instruction indexing." ) elif is_usdc: + logger.debug(f"index_payment_router.py | Parsing memos: {memos}") memo = parse_route_transaction_memo( session=session, memos=memos, timestamp=timestamp ) @@ -874,6 +881,7 @@ def process_payment_router_txs() -> None: get_sol_tx_info, solana_client_manager, str(tx_sig), + redis ): tx_sig for tx_sig in tx_sig_batch } diff --git a/packages/discovery-provider/src/utils/helpers.py b/packages/discovery-provider/src/utils/helpers.py index e98bb5fb72d..1198c929ed3 100644 --- a/packages/discovery-provider/src/utils/helpers.py +++ b/packages/discovery-provider/src/utils/helpers.py @@ -25,7 +25,7 @@ from web3 import Web3 from src import exceptions -from src.solana.solana_helpers import MEMO_PROGRAM_ID +from src.solana.solana_helpers import MEMO_PROGRAM_ID, MEMO_V2_PROGRAM_ID from . import multihash @@ -500,20 +500,32 @@ def get_solana_tx_token_balance_changes( return balance_changes -def decode_all_solana_memos(tx_message: Message): - """Finds all memo instructions in a transaction and base58 decodes their instruction data as a string""" +def get_memo_program_index(tx_message: Message): try: - memo_program_index = tx_message.account_keys.index( - Pubkey.from_string(MEMO_PROGRAM_ID) - ) - return [ - base58.b58decode(instruction.data).decode("utf8") - for instruction in tx_message.instructions - if instruction.program_id_index == memo_program_index - ] - except: + return tx_message.account_keys.index(Pubkey.from_string(MEMO_PROGRAM_ID)) + except ValueError: + # Do nothing, there's no memos + return None + + +def get_memo_v2_program_index(tx_message: Message): + try: + return tx_message.account_keys.index(Pubkey.from_string(MEMO_V2_PROGRAM_ID)) + except ValueError: # Do nothing, there's no memos - return [] + return None + + +def decode_all_solana_memos(tx_message: Message): + """Finds all memo instructions in a transaction and base58 decodes their instruction data as a string""" + memo_program_index = get_memo_program_index(tx_message) + memo_v2_program_index = get_memo_v2_program_index(tx_message) + return [ + base58.b58decode(instruction.data).decode("utf8") + for instruction in tx_message.instructions + if instruction.program_id_index == memo_program_index + or instruction.program_id_index == memo_v2_program_index + ] def get_account_owner_from_balance_change( diff --git a/packages/discovery-provider/src/utils/web3_provider.py b/packages/discovery-provider/src/utils/web3_provider.py index 09075291b46..f5f9b56b689 100644 --- a/packages/discovery-provider/src/utils/web3_provider.py +++ b/packages/discovery-provider/src/utils/web3_provider.py @@ -3,6 +3,7 @@ """ import logging +import math import os from typing import Optional @@ -17,6 +18,8 @@ web3: Optional[Web3] = None +GATEWAY_FALLBACK_BLOCKDIFF = 10000 + def get_web3(web3endpoint=None): # pylint: disable=W0603 @@ -25,21 +28,28 @@ def get_web3(web3endpoint=None): if web3: return web3 - if not web3endpoint: + if web3endpoint: + web3 = Web3(HTTPProvider(web3endpoint)) + else: + local_rpc = os.getenv("audius_web3_localhost") + local_web3 = Web3(HTTPProvider(local_rpc)) + gateway_web3 = Web3(HTTPProvider(os.getenv("audius_web3_host"))) # attempt local rpc, check if healthy try: - local_rpc = os.getenv("audius_web3_localhost") - if requests.get(local_rpc + "/health").status_code == 200: - web3endpoint = local_rpc + block_diff = math.abs( + local_web3.eth.get_block_number() - gateway_web3.eth.get_block_number() + ) + resp = requests.get(local_rpc + "/health") + if resp.status_code == 200 and block_diff < GATEWAY_FALLBACK_BLOCKDIFF: + web3 = local_web3 logger.info("web3_provider.py | using local RPC") else: raise Exception("local RPC unhealthy or unreachable") except Exception as e: - web3endpoint = os.getenv("audius_web3_host") logger.warn(e) - web3 = Web3(HTTPProvider(web3endpoint)) - web3.strict_bytes_type_checking = False + web3 = gateway_web3 + web3.strict_bytes_type_checking = False # required middleware for POA # https://web3py.readthedocs.io/en/latest/middleware.html#proof-of-authority web3.middleware_onion.inject(geth_poa_middleware, layer=0) diff --git a/packages/harmony/src/components/button/FilterButton/FilterButton.tsx b/packages/harmony/src/components/button/FilterButton/FilterButton.tsx index d6cbb7577cb..8aa4a7213d3 100644 --- a/packages/harmony/src/components/button/FilterButton/FilterButton.tsx +++ b/packages/harmony/src/components/button/FilterButton/FilterButton.tsx @@ -84,7 +84,7 @@ export const FilterButton = forwardRef( borderRadius: cornerRadius.s, color: variant === 'fillContainer' && selection !== null - ? color.special.white + ? color.static.white : color.text.default, gap: spacing.xs, fontSize: typography.size.s, diff --git a/packages/harmony/src/components/text-link/TextLink.tsx b/packages/harmony/src/components/text-link/TextLink.tsx index c0bff56c2af..5399a907174 100644 --- a/packages/harmony/src/components/text-link/TextLink.tsx +++ b/packages/harmony/src/components/text-link/TextLink.tsx @@ -21,6 +21,7 @@ export const TextLink = forwardRef((props: TextLinkProps, ref: Ref<'a'>) => { textVariant, showUnderline, applyHoverStylesToInnerSvg, + disabled, ...other } = props @@ -66,6 +67,7 @@ export const TextLink = forwardRef((props: TextLinkProps, ref: Ref<'a'>) => { color: variantColors[variant], textDecoration: 'none', transition: `color ${motion.hover}`, + pointerEvents: disabled ? 'none' : undefined, ':hover': hoverStyles, ...(isActive && { ...hoverStyles, textDecoration: 'none' }), ...(showUnderline && hoverStyles) diff --git a/packages/libs/scripts/check-generated-types.sh b/packages/libs/scripts/check-generated-types.sh index 32f8ef48640..99371e84e26 100755 --- a/packages/libs/scripts/check-generated-types.sh +++ b/packages/libs/scripts/check-generated-types.sh @@ -8,5 +8,6 @@ if [ -z "$(git status . --porcelain)" ]; then printf '%s\n' "No diff found between generated types and checked in types" else printf '%s\n' "Found diff between generated types and checked in types, please 'npm run gen:dev' in libs" >&2 + git diff . exit 1 fi diff --git a/packages/libs/src/sdk/api/generated/default/.openapi-generator/FILES b/packages/libs/src/sdk/api/generated/default/.openapi-generator/FILES index 93ae174f9b8..42fa3996515 100644 --- a/packages/libs/src/sdk/api/generated/default/.openapi-generator/FILES +++ b/packages/libs/src/sdk/api/generated/default/.openapi-generator/FILES @@ -1,3 +1,4 @@ +.openapi-generator-ignore apis/DashboardWalletUsersApi.ts apis/DeveloperAppsApi.ts apis/PlaylistsApi.ts @@ -8,6 +9,7 @@ apis/UsersApi.ts apis/index.ts index.ts models/Access.ts +models/AccessGate.ts models/Activity.ts models/AuthorizedApp.ts models/AuthorizedApps.ts @@ -24,11 +26,14 @@ models/DeveloperApps.ts models/EncodedUserId.ts models/Favorite.ts models/FavoritesResponse.ts +models/FollowGate.ts models/FollowersResponse.ts models/FollowingResponse.ts models/GetSupporters.ts models/GetSupporting.ts models/GetTipsResponse.ts +models/NftCollection.ts +models/NftGate.ts models/Playlist.ts models/PlaylistAddedTimestamp.ts models/PlaylistArtwork.ts @@ -36,6 +41,7 @@ models/PlaylistResponse.ts models/PlaylistSearchResult.ts models/PlaylistTracksResponse.ts models/ProfilePicture.ts +models/PurchaseGate.ts models/RelatedArtistResponse.ts models/RemixParent.ts models/Reposts.ts @@ -44,6 +50,7 @@ models/Supporter.ts models/Supporting.ts models/TagsResponse.ts models/Tip.ts +models/TipGate.ts models/TopListener.ts models/Track.ts models/TrackArtwork.ts @@ -53,6 +60,7 @@ models/TrackResponse.ts models/TrackSearch.ts models/TracksResponse.ts models/TrendingPlaylistsResponse.ts +models/UsdcGate.ts models/User.ts models/UserAssociatedWalletResponse.ts models/UserResponse.ts diff --git a/packages/libs/src/sdk/api/generated/default/.openapi-generator/VERSION b/packages/libs/src/sdk/api/generated/default/.openapi-generator/VERSION index 08bfd0643b8..18bb4182dd0 100644 --- a/packages/libs/src/sdk/api/generated/default/.openapi-generator/VERSION +++ b/packages/libs/src/sdk/api/generated/default/.openapi-generator/VERSION @@ -1 +1 @@ -7.5.0-SNAPSHOT +7.5.0 diff --git a/packages/libs/src/sdk/api/generated/default/models/AccessGate.ts b/packages/libs/src/sdk/api/generated/default/models/AccessGate.ts new file mode 100644 index 00000000000..f27d886ea79 --- /dev/null +++ b/packages/libs/src/sdk/api/generated/default/models/AccessGate.ts @@ -0,0 +1,86 @@ +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck +/** + * API + * Audius V1 API + * + * The version of the OpenAPI document: 1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { + FollowGate, + instanceOfFollowGate, + FollowGateFromJSON, + FollowGateFromJSONTyped, + FollowGateToJSON, +} from './FollowGate'; +import { + NftGate, + instanceOfNftGate, + NftGateFromJSON, + NftGateFromJSONTyped, + NftGateToJSON, +} from './NftGate'; +import { + PurchaseGate, + instanceOfPurchaseGate, + PurchaseGateFromJSON, + PurchaseGateFromJSONTyped, + PurchaseGateToJSON, +} from './PurchaseGate'; +import { + TipGate, + instanceOfTipGate, + TipGateFromJSON, + TipGateFromJSONTyped, + TipGateToJSON, +} from './TipGate'; + +/** + * @type AccessGate + * + * @export + */ +export type AccessGate = FollowGate | NftGate | PurchaseGate | TipGate; + +export function AccessGateFromJSON(json: any): AccessGate { + return AccessGateFromJSONTyped(json, false); +} + +export function AccessGateFromJSONTyped(json: any, ignoreDiscriminator: boolean): AccessGate { + if ((json === undefined) || (json === null)) { + return json; + } + return { ...FollowGateFromJSONTyped(json, true), ...NftGateFromJSONTyped(json, true), ...PurchaseGateFromJSONTyped(json, true), ...TipGateFromJSONTyped(json, true) }; +} + +export function AccessGateToJSON(value?: AccessGate | null): any { + if (value === undefined) { + return undefined; + } + if (value === null) { + return null; + } + + if (instanceOfFollowGate(value)) { + return FollowGateToJSON(value as FollowGate); + } + if (instanceOfNftGate(value)) { + return NftGateToJSON(value as NftGate); + } + if (instanceOfPurchaseGate(value)) { + return PurchaseGateToJSON(value as PurchaseGate); + } + if (instanceOfTipGate(value)) { + return TipGateToJSON(value as TipGate); + } + + return {}; +} + diff --git a/packages/libs/src/sdk/api/generated/default/models/BulkSubscribersResponse.ts b/packages/libs/src/sdk/api/generated/default/models/BulkSubscribersResponse.ts deleted file mode 100644 index 496e5f96fb9..00000000000 --- a/packages/libs/src/sdk/api/generated/default/models/BulkSubscribersResponse.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -// @ts-nocheck -/** - * API - * Audius V1 API - * - * The version of the OpenAPI document: 1.0 - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - -import { exists, mapValues } from '../runtime'; -import type { UserSubscribers } from './UserSubscribers'; -import { - UserSubscribersFromJSON, - UserSubscribersFromJSONTyped, - UserSubscribersToJSON, -} from './UserSubscribers'; - -/** - * - * @export - * @interface BulkSubscribersResponse - */ -export interface BulkSubscribersResponse { - /** - * - * @type {Array} - * @memberof BulkSubscribersResponse - */ - data?: Array; -} - -/** - * Check if a given object implements the BulkSubscribersResponse interface. - */ -export function instanceOfBulkSubscribersResponse(value: object): boolean { - let isInstance = true; - - return isInstance; -} - -export function BulkSubscribersResponseFromJSON(json: any): BulkSubscribersResponse { - return BulkSubscribersResponseFromJSONTyped(json, false); -} - -export function BulkSubscribersResponseFromJSONTyped(json: any, ignoreDiscriminator: boolean): BulkSubscribersResponse { - if ((json === undefined) || (json === null)) { - return json; - } - return { - - 'data': !exists(json, 'data') ? undefined : ((json['data'] as Array).map(UserSubscribersFromJSON)), - }; -} - -export function BulkSubscribersResponseToJSON(value?: BulkSubscribersResponse | null): any { - if (value === undefined) { - return undefined; - } - if (value === null) { - return null; - } - return { - - 'data': value.data === undefined ? undefined : ((value.data as Array).map(UserSubscribersToJSON)), - }; -} - diff --git a/packages/libs/src/sdk/api/generated/default/models/FollowGate.ts b/packages/libs/src/sdk/api/generated/default/models/FollowGate.ts new file mode 100644 index 00000000000..f1963185827 --- /dev/null +++ b/packages/libs/src/sdk/api/generated/default/models/FollowGate.ts @@ -0,0 +1,67 @@ +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck +/** + * API + * Audius V1 API + * + * The version of the OpenAPI document: 1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { exists, mapValues } from '../runtime'; +/** + * + * @export + * @interface FollowGate + */ +export interface FollowGate { + /** + * Must follow the given user ID to unlock + * @type {number} + * @memberof FollowGate + */ + followUserId: number; +} + +/** + * Check if a given object implements the FollowGate interface. + */ +export function instanceOfFollowGate(value: object): boolean { + let isInstance = true; + isInstance = isInstance && "followUserId" in value; + + return isInstance; +} + +export function FollowGateFromJSON(json: any): FollowGate { + return FollowGateFromJSONTyped(json, false); +} + +export function FollowGateFromJSONTyped(json: any, ignoreDiscriminator: boolean): FollowGate { + if ((json === undefined) || (json === null)) { + return json; + } + return { + + 'followUserId': json['follow_user_id'], + }; +} + +export function FollowGateToJSON(value?: FollowGate | null): any { + if (value === undefined) { + return undefined; + } + if (value === null) { + return null; + } + return { + + 'follow_user_id': value.followUserId, + }; +} + diff --git a/packages/libs/src/sdk/api/generated/default/models/GetSupporter.ts b/packages/libs/src/sdk/api/generated/default/models/GetSupporter.ts deleted file mode 100644 index c6dc0c4d135..00000000000 --- a/packages/libs/src/sdk/api/generated/default/models/GetSupporter.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -// @ts-nocheck -/** - * API - * Audius V1 API - * - * The version of the OpenAPI document: 1.0 - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - -import { exists, mapValues } from '../runtime'; -import type { Supporter } from './Supporter'; -import { - SupporterFromJSON, - SupporterFromJSONTyped, - SupporterToJSON, -} from './Supporter'; - -/** - * - * @export - * @interface GetSupporter - */ -export interface GetSupporter { - /** - * - * @type {Supporter} - * @memberof GetSupporter - */ - data?: Supporter; -} - -/** - * Check if a given object implements the GetSupporter interface. - */ -export function instanceOfGetSupporter(value: object): boolean { - let isInstance = true; - - return isInstance; -} - -export function GetSupporterFromJSON(json: any): GetSupporter { - return GetSupporterFromJSONTyped(json, false); -} - -export function GetSupporterFromJSONTyped(json: any, ignoreDiscriminator: boolean): GetSupporter { - if ((json === undefined) || (json === null)) { - return json; - } - return { - - 'data': !exists(json, 'data') ? undefined : SupporterFromJSON(json['data']), - }; -} - -export function GetSupporterToJSON(value?: GetSupporter | null): any { - if (value === undefined) { - return undefined; - } - if (value === null) { - return null; - } - return { - - 'data': SupporterToJSON(value.data), - }; -} - diff --git a/packages/libs/src/sdk/api/generated/default/models/Grant.ts b/packages/libs/src/sdk/api/generated/default/models/Grant.ts deleted file mode 100644 index 2c2fee58696..00000000000 --- a/packages/libs/src/sdk/api/generated/default/models/Grant.ts +++ /dev/null @@ -1,112 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -// @ts-nocheck -/** - * API - * Audius V1 API - * - * The version of the OpenAPI document: 1.0 - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - -import { exists, mapValues } from '../runtime'; -/** - * - * @export - * @interface Grant - */ -export interface Grant { - /** - * - * @type {string} - * @memberof Grant - */ - granteeAddress: string; - /** - * - * @type {string} - * @memberof Grant - */ - userId: string; - /** - * - * @type {boolean} - * @memberof Grant - */ - isRevoked: boolean; - /** - * - * @type {boolean} - * @memberof Grant - */ - isApproved: boolean; - /** - * - * @type {string} - * @memberof Grant - */ - createdAt: string; - /** - * - * @type {string} - * @memberof Grant - */ - updatedAt: string; -} - -/** - * Check if a given object implements the Grant interface. - */ -export function instanceOfGrant(value: object): boolean { - let isInstance = true; - isInstance = isInstance && "granteeAddress" in value; - isInstance = isInstance && "userId" in value; - isInstance = isInstance && "isRevoked" in value; - isInstance = isInstance && "isApproved" in value; - isInstance = isInstance && "createdAt" in value; - isInstance = isInstance && "updatedAt" in value; - - return isInstance; -} - -export function GrantFromJSON(json: any): Grant { - return GrantFromJSONTyped(json, false); -} - -export function GrantFromJSONTyped(json: any, ignoreDiscriminator: boolean): Grant { - if ((json === undefined) || (json === null)) { - return json; - } - return { - - 'granteeAddress': json['grantee_address'], - 'userId': json['user_id'], - 'isRevoked': json['is_revoked'], - 'isApproved': json['is_approved'], - 'createdAt': json['created_at'], - 'updatedAt': json['updated_at'], - }; -} - -export function GrantToJSON(value?: Grant | null): any { - if (value === undefined) { - return undefined; - } - if (value === null) { - return null; - } - return { - - 'grantee_address': value.granteeAddress, - 'user_id': value.userId, - 'is_revoked': value.isRevoked, - 'is_approved': value.isApproved, - 'created_at': value.createdAt, - 'updated_at': value.updatedAt, - }; -} - diff --git a/packages/libs/src/sdk/api/generated/default/models/HistoryResponse.ts b/packages/libs/src/sdk/api/generated/default/models/HistoryResponse.ts deleted file mode 100644 index 7998857b90d..00000000000 --- a/packages/libs/src/sdk/api/generated/default/models/HistoryResponse.ts +++ /dev/null @@ -1,142 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -// @ts-nocheck -/** - * API - * Audius V1 API - * - * The version of the OpenAPI document: 1.0 - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - -import { exists, mapValues } from '../runtime'; -import type { Activity } from './Activity'; -import { - ActivityFromJSON, - ActivityFromJSONTyped, - ActivityToJSON, -} from './Activity'; -import type { VersionMetadata } from './VersionMetadata'; -import { - VersionMetadataFromJSON, - VersionMetadataFromJSONTyped, - VersionMetadataToJSON, -} from './VersionMetadata'; - -/** - * - * @export - * @interface HistoryResponse - */ -export interface HistoryResponse { - /** - * - * @type {number} - * @memberof HistoryResponse - */ - latestChainBlock: number; - /** - * - * @type {number} - * @memberof HistoryResponse - */ - latestIndexedBlock: number; - /** - * - * @type {number} - * @memberof HistoryResponse - */ - latestChainSlotPlays: number; - /** - * - * @type {number} - * @memberof HistoryResponse - */ - latestIndexedSlotPlays: number; - /** - * - * @type {string} - * @memberof HistoryResponse - */ - signature: string; - /** - * - * @type {string} - * @memberof HistoryResponse - */ - timestamp: string; - /** - * - * @type {VersionMetadata} - * @memberof HistoryResponse - */ - version: VersionMetadata; - /** - * - * @type {Array} - * @memberof HistoryResponse - */ - data?: Array; -} - -/** - * Check if a given object implements the HistoryResponse interface. - */ -export function instanceOfHistoryResponse(value: object): boolean { - let isInstance = true; - isInstance = isInstance && "latestChainBlock" in value; - isInstance = isInstance && "latestIndexedBlock" in value; - isInstance = isInstance && "latestChainSlotPlays" in value; - isInstance = isInstance && "latestIndexedSlotPlays" in value; - isInstance = isInstance && "signature" in value; - isInstance = isInstance && "timestamp" in value; - isInstance = isInstance && "version" in value; - - return isInstance; -} - -export function HistoryResponseFromJSON(json: any): HistoryResponse { - return HistoryResponseFromJSONTyped(json, false); -} - -export function HistoryResponseFromJSONTyped(json: any, ignoreDiscriminator: boolean): HistoryResponse { - if ((json === undefined) || (json === null)) { - return json; - } - return { - - 'latestChainBlock': json['latest_chain_block'], - 'latestIndexedBlock': json['latest_indexed_block'], - 'latestChainSlotPlays': json['latest_chain_slot_plays'], - 'latestIndexedSlotPlays': json['latest_indexed_slot_plays'], - 'signature': json['signature'], - 'timestamp': json['timestamp'], - 'version': VersionMetadataFromJSON(json['version']), - 'data': !exists(json, 'data') ? undefined : ((json['data'] as Array).map(ActivityFromJSON)), - }; -} - -export function HistoryResponseToJSON(value?: HistoryResponse | null): any { - if (value === undefined) { - return undefined; - } - if (value === null) { - return null; - } - return { - - 'latest_chain_block': value.latestChainBlock, - 'latest_indexed_block': value.latestIndexedBlock, - 'latest_chain_slot_plays': value.latestChainSlotPlays, - 'latest_indexed_slot_plays': value.latestIndexedSlotPlays, - 'signature': value.signature, - 'timestamp': value.timestamp, - 'version': VersionMetadataToJSON(value.version), - 'data': value.data === undefined ? undefined : ((value.data as Array).map(ActivityToJSON)), - }; -} - diff --git a/packages/libs/src/sdk/api/generated/default/models/ListenCount.ts b/packages/libs/src/sdk/api/generated/default/models/ListenCount.ts deleted file mode 100644 index 57c6efa2aab..00000000000 --- a/packages/libs/src/sdk/api/generated/default/models/ListenCount.ts +++ /dev/null @@ -1,82 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -// @ts-nocheck -/** - * API - * Audius V1 API - * - * The version of the OpenAPI document: 1.0 - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - -import { exists, mapValues } from '../runtime'; -/** - * - * @export - * @interface ListenCount - */ -export interface ListenCount { - /** - * - * @type {number} - * @memberof ListenCount - */ - trackId?: number; - /** - * - * @type {string} - * @memberof ListenCount - */ - date?: string; - /** - * - * @type {number} - * @memberof ListenCount - */ - listens?: number; -} - -/** - * Check if a given object implements the ListenCount interface. - */ -export function instanceOfListenCount(value: object): boolean { - let isInstance = true; - - return isInstance; -} - -export function ListenCountFromJSON(json: any): ListenCount { - return ListenCountFromJSONTyped(json, false); -} - -export function ListenCountFromJSONTyped(json: any, ignoreDiscriminator: boolean): ListenCount { - if ((json === undefined) || (json === null)) { - return json; - } - return { - - 'trackId': !exists(json, 'trackId') ? undefined : json['trackId'], - 'date': !exists(json, 'date') ? undefined : json['date'], - 'listens': !exists(json, 'listens') ? undefined : json['listens'], - }; -} - -export function ListenCountToJSON(value?: ListenCount | null): any { - if (value === undefined) { - return undefined; - } - if (value === null) { - return null; - } - return { - - 'trackId': value.trackId, - 'date': value.date, - 'listens': value.listens, - }; -} - diff --git a/packages/libs/src/sdk/api/generated/default/models/ManagedUser.ts b/packages/libs/src/sdk/api/generated/default/models/ManagedUser.ts deleted file mode 100644 index e8cb0110e5d..00000000000 --- a/packages/libs/src/sdk/api/generated/default/models/ManagedUser.ts +++ /dev/null @@ -1,89 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -// @ts-nocheck -/** - * API - * Audius V1 API - * - * The version of the OpenAPI document: 1.0 - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - -import { exists, mapValues } from '../runtime'; -import type { Grant } from './Grant'; -import { - GrantFromJSON, - GrantFromJSONTyped, - GrantToJSON, -} from './Grant'; -import type { UserFull } from './UserFull'; -import { - UserFullFromJSON, - UserFullFromJSONTyped, - UserFullToJSON, -} from './UserFull'; - -/** - * - * @export - * @interface ManagedUser - */ -export interface ManagedUser { - /** - * - * @type {UserFull} - * @memberof ManagedUser - */ - user: UserFull; - /** - * - * @type {Grant} - * @memberof ManagedUser - */ - grant: Grant; -} - -/** - * Check if a given object implements the ManagedUser interface. - */ -export function instanceOfManagedUser(value: object): boolean { - let isInstance = true; - isInstance = isInstance && "user" in value; - isInstance = isInstance && "grant" in value; - - return isInstance; -} - -export function ManagedUserFromJSON(json: any): ManagedUser { - return ManagedUserFromJSONTyped(json, false); -} - -export function ManagedUserFromJSONTyped(json: any, ignoreDiscriminator: boolean): ManagedUser { - if ((json === undefined) || (json === null)) { - return json; - } - return { - - 'user': UserFullFromJSON(json['user']), - 'grant': GrantFromJSON(json['grant']), - }; -} - -export function ManagedUserToJSON(value?: ManagedUser | null): any { - if (value === undefined) { - return undefined; - } - if (value === null) { - return null; - } - return { - - 'user': UserFullToJSON(value.user), - 'grant': GrantToJSON(value.grant), - }; -} - diff --git a/packages/libs/src/sdk/api/generated/default/models/ManagedUsers.ts b/packages/libs/src/sdk/api/generated/default/models/ManagedUsers.ts deleted file mode 100644 index 97d457c9afb..00000000000 --- a/packages/libs/src/sdk/api/generated/default/models/ManagedUsers.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -// @ts-nocheck -/** - * API - * Audius V1 API - * - * The version of the OpenAPI document: 1.0 - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - -import { exists, mapValues } from '../runtime'; -import type { ManagedUser } from './ManagedUser'; -import { - ManagedUserFromJSON, - ManagedUserFromJSONTyped, - ManagedUserToJSON, -} from './ManagedUser'; - -/** - * - * @export - * @interface ManagedUsers - */ -export interface ManagedUsers { - /** - * - * @type {Array} - * @memberof ManagedUsers - */ - data?: Array; -} - -/** - * Check if a given object implements the ManagedUsers interface. - */ -export function instanceOfManagedUsers(value: object): boolean { - let isInstance = true; - - return isInstance; -} - -export function ManagedUsersFromJSON(json: any): ManagedUsers { - return ManagedUsersFromJSONTyped(json, false); -} - -export function ManagedUsersFromJSONTyped(json: any, ignoreDiscriminator: boolean): ManagedUsers { - if ((json === undefined) || (json === null)) { - return json; - } - return { - - 'data': !exists(json, 'data') ? undefined : ((json['data'] as Array).map(ManagedUserFromJSON)), - }; -} - -export function ManagedUsersToJSON(value?: ManagedUsers | null): any { - if (value === undefined) { - return undefined; - } - if (value === null) { - return null; - } - return { - - 'data': value.data === undefined ? undefined : ((value.data as Array).map(ManagedUserToJSON)), - }; -} - diff --git a/packages/libs/src/sdk/api/generated/default/models/MonthlyAggregatePlay.ts b/packages/libs/src/sdk/api/generated/default/models/MonthlyAggregatePlay.ts deleted file mode 100644 index 27fa2a7ea3c..00000000000 --- a/packages/libs/src/sdk/api/generated/default/models/MonthlyAggregatePlay.ts +++ /dev/null @@ -1,89 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -// @ts-nocheck -/** - * API - * Audius V1 API - * - * The version of the OpenAPI document: 1.0 - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - -import { exists, mapValues } from '../runtime'; -import type { ListenCount } from './ListenCount'; -import { - ListenCountFromJSON, - ListenCountFromJSONTyped, - ListenCountToJSON, -} from './ListenCount'; - -/** - * - * @export - * @interface MonthlyAggregatePlay - */ -export interface MonthlyAggregatePlay { - /** - * - * @type {number} - * @memberof MonthlyAggregatePlay - */ - totalListens?: number; - /** - * - * @type {Array} - * @memberof MonthlyAggregatePlay - */ - trackIds?: Array; - /** - * - * @type {Array} - * @memberof MonthlyAggregatePlay - */ - listenCounts?: Array; -} - -/** - * Check if a given object implements the MonthlyAggregatePlay interface. - */ -export function instanceOfMonthlyAggregatePlay(value: object): boolean { - let isInstance = true; - - return isInstance; -} - -export function MonthlyAggregatePlayFromJSON(json: any): MonthlyAggregatePlay { - return MonthlyAggregatePlayFromJSONTyped(json, false); -} - -export function MonthlyAggregatePlayFromJSONTyped(json: any, ignoreDiscriminator: boolean): MonthlyAggregatePlay { - if ((json === undefined) || (json === null)) { - return json; - } - return { - - 'totalListens': !exists(json, 'totalListens') ? undefined : json['totalListens'], - 'trackIds': !exists(json, 'trackIds') ? undefined : json['trackIds'], - 'listenCounts': !exists(json, 'listenCounts') ? undefined : ((json['listenCounts'] as Array).map(ListenCountFromJSON)), - }; -} - -export function MonthlyAggregatePlayToJSON(value?: MonthlyAggregatePlay | null): any { - if (value === undefined) { - return undefined; - } - if (value === null) { - return null; - } - return { - - 'totalListens': value.totalListens, - 'trackIds': value.trackIds, - 'listenCounts': value.listenCounts === undefined ? undefined : ((value.listenCounts as Array).map(ListenCountToJSON)), - }; -} - diff --git a/packages/libs/src/sdk/api/generated/default/models/NftCollection.ts b/packages/libs/src/sdk/api/generated/default/models/NftCollection.ts new file mode 100644 index 00000000000..0de5cd93ce4 --- /dev/null +++ b/packages/libs/src/sdk/api/generated/default/models/NftCollection.ts @@ -0,0 +1,112 @@ +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck +/** + * API + * Audius V1 API + * + * The version of the OpenAPI document: 1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { exists, mapValues } from '../runtime'; +/** + * + * @export + * @interface NftCollection + */ +export interface NftCollection { + /** + * + * @type {string} + * @memberof NftCollection + */ + chain: NftCollectionChainEnum; + /** + * + * @type {string} + * @memberof NftCollection + */ + address: string; + /** + * + * @type {string} + * @memberof NftCollection + */ + name: string; + /** + * + * @type {string} + * @memberof NftCollection + */ + imageUrl?: string; + /** + * + * @type {string} + * @memberof NftCollection + */ + externalLink?: string; +} + + +/** + * @export + */ +export const NftCollectionChainEnum = { + Eth: 'eth', + Sol: 'sol' +} as const; +export type NftCollectionChainEnum = typeof NftCollectionChainEnum[keyof typeof NftCollectionChainEnum]; + + +/** + * Check if a given object implements the NftCollection interface. + */ +export function instanceOfNftCollection(value: object): boolean { + let isInstance = true; + isInstance = isInstance && "chain" in value; + isInstance = isInstance && "address" in value; + isInstance = isInstance && "name" in value; + + return isInstance; +} + +export function NftCollectionFromJSON(json: any): NftCollection { + return NftCollectionFromJSONTyped(json, false); +} + +export function NftCollectionFromJSONTyped(json: any, ignoreDiscriminator: boolean): NftCollection { + if ((json === undefined) || (json === null)) { + return json; + } + return { + + 'chain': json['chain'], + 'address': json['address'], + 'name': json['name'], + 'imageUrl': !exists(json, 'imageUrl') ? undefined : json['imageUrl'], + 'externalLink': !exists(json, 'externalLink') ? undefined : json['externalLink'], + }; +} + +export function NftCollectionToJSON(value?: NftCollection | null): any { + if (value === undefined) { + return undefined; + } + if (value === null) { + return null; + } + return { + + 'chain': value.chain, + 'address': value.address, + 'name': value.name, + 'imageUrl': value.imageUrl, + 'externalLink': value.externalLink, + }; +} + diff --git a/packages/libs/src/sdk/api/generated/default/models/NftGate.ts b/packages/libs/src/sdk/api/generated/default/models/NftGate.ts new file mode 100644 index 00000000000..44e2fe37fbc --- /dev/null +++ b/packages/libs/src/sdk/api/generated/default/models/NftGate.ts @@ -0,0 +1,74 @@ +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck +/** + * API + * Audius V1 API + * + * The version of the OpenAPI document: 1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { exists, mapValues } from '../runtime'; +import type { NftCollection } from './NftCollection'; +import { + NftCollectionFromJSON, + NftCollectionFromJSONTyped, + NftCollectionToJSON, +} from './NftCollection'; + +/** + * + * @export + * @interface NftGate + */ +export interface NftGate { + /** + * Must hold an NFT of the given collection to unlock + * @type {NftCollection} + * @memberof NftGate + */ + nftCollection: NftCollection; +} + +/** + * Check if a given object implements the NftGate interface. + */ +export function instanceOfNftGate(value: object): boolean { + let isInstance = true; + isInstance = isInstance && "nftCollection" in value; + + return isInstance; +} + +export function NftGateFromJSON(json: any): NftGate { + return NftGateFromJSONTyped(json, false); +} + +export function NftGateFromJSONTyped(json: any, ignoreDiscriminator: boolean): NftGate { + if ((json === undefined) || (json === null)) { + return json; + } + return { + + 'nftCollection': NftCollectionFromJSON(json['nft_collection']), + }; +} + +export function NftGateToJSON(value?: NftGate | null): any { + if (value === undefined) { + return undefined; + } + if (value === null) { + return null; + } + return { + + 'nft_collection': NftCollectionToJSON(value.nftCollection), + }; +} + diff --git a/packages/libs/src/sdk/api/generated/default/models/PurchaseGate.ts b/packages/libs/src/sdk/api/generated/default/models/PurchaseGate.ts new file mode 100644 index 00000000000..68b48109543 --- /dev/null +++ b/packages/libs/src/sdk/api/generated/default/models/PurchaseGate.ts @@ -0,0 +1,74 @@ +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck +/** + * API + * Audius V1 API + * + * The version of the OpenAPI document: 1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { exists, mapValues } from '../runtime'; +import type { UsdcGate } from './UsdcGate'; +import { + UsdcGateFromJSON, + UsdcGateFromJSONTyped, + UsdcGateToJSON, +} from './UsdcGate'; + +/** + * + * @export + * @interface PurchaseGate + */ +export interface PurchaseGate { + /** + * Must pay the total price and split to the given addresses to unlock + * @type {UsdcGate} + * @memberof PurchaseGate + */ + usdcPurchase: UsdcGate; +} + +/** + * Check if a given object implements the PurchaseGate interface. + */ +export function instanceOfPurchaseGate(value: object): boolean { + let isInstance = true; + isInstance = isInstance && "usdcPurchase" in value; + + return isInstance; +} + +export function PurchaseGateFromJSON(json: any): PurchaseGate { + return PurchaseGateFromJSONTyped(json, false); +} + +export function PurchaseGateFromJSONTyped(json: any, ignoreDiscriminator: boolean): PurchaseGate { + if ((json === undefined) || (json === null)) { + return json; + } + return { + + 'usdcPurchase': UsdcGateFromJSON(json['usdc_purchase']), + }; +} + +export function PurchaseGateToJSON(value?: PurchaseGate | null): any { + if (value === undefined) { + return undefined; + } + if (value === null) { + return null; + } + return { + + 'usdc_purchase': UsdcGateToJSON(value.usdcPurchase), + }; +} + diff --git a/packages/libs/src/sdk/api/generated/default/models/TipGate.ts b/packages/libs/src/sdk/api/generated/default/models/TipGate.ts new file mode 100644 index 00000000000..1e88d4c99b3 --- /dev/null +++ b/packages/libs/src/sdk/api/generated/default/models/TipGate.ts @@ -0,0 +1,67 @@ +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck +/** + * API + * Audius V1 API + * + * The version of the OpenAPI document: 1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { exists, mapValues } from '../runtime'; +/** + * + * @export + * @interface TipGate + */ +export interface TipGate { + /** + * Must tip the given user ID to unlock + * @type {number} + * @memberof TipGate + */ + tipUserId: number; +} + +/** + * Check if a given object implements the TipGate interface. + */ +export function instanceOfTipGate(value: object): boolean { + let isInstance = true; + isInstance = isInstance && "tipUserId" in value; + + return isInstance; +} + +export function TipGateFromJSON(json: any): TipGate { + return TipGateFromJSONTyped(json, false); +} + +export function TipGateFromJSONTyped(json: any, ignoreDiscriminator: boolean): TipGate { + if ((json === undefined) || (json === null)) { + return json; + } + return { + + 'tipUserId': json['tip_user_id'], + }; +} + +export function TipGateToJSON(value?: TipGate | null): any { + if (value === undefined) { + return undefined; + } + if (value === null) { + return null; + } + return { + + 'tip_user_id': value.tipUserId, + }; +} + diff --git a/packages/libs/src/sdk/api/generated/default/models/TopGenreUsersResponse.ts b/packages/libs/src/sdk/api/generated/default/models/TopGenreUsersResponse.ts deleted file mode 100644 index fa894de233d..00000000000 --- a/packages/libs/src/sdk/api/generated/default/models/TopGenreUsersResponse.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -// @ts-nocheck -/** - * API - * Audius V1 API - * - * The version of the OpenAPI document: 1.0 - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - -import { exists, mapValues } from '../runtime'; -import type { User } from './User'; -import { - UserFromJSON, - UserFromJSONTyped, - UserToJSON, -} from './User'; - -/** - * - * @export - * @interface TopGenreUsersResponse - */ -export interface TopGenreUsersResponse { - /** - * - * @type {Array} - * @memberof TopGenreUsersResponse - */ - data?: Array; -} - -/** - * Check if a given object implements the TopGenreUsersResponse interface. - */ -export function instanceOfTopGenreUsersResponse(value: object): boolean { - let isInstance = true; - - return isInstance; -} - -export function TopGenreUsersResponseFromJSON(json: any): TopGenreUsersResponse { - return TopGenreUsersResponseFromJSONTyped(json, false); -} - -export function TopGenreUsersResponseFromJSONTyped(json: any, ignoreDiscriminator: boolean): TopGenreUsersResponse { - if ((json === undefined) || (json === null)) { - return json; - } - return { - - 'data': !exists(json, 'data') ? undefined : ((json['data'] as Array).map(UserFromJSON)), - }; -} - -export function TopGenreUsersResponseToJSON(value?: TopGenreUsersResponse | null): any { - if (value === undefined) { - return undefined; - } - if (value === null) { - return null; - } - return { - - 'data': value.data === undefined ? undefined : ((value.data as Array).map(UserToJSON)), - }; -} - diff --git a/packages/libs/src/sdk/api/generated/default/models/TopUsersResponse.ts b/packages/libs/src/sdk/api/generated/default/models/TopUsersResponse.ts deleted file mode 100644 index a3b5705f864..00000000000 --- a/packages/libs/src/sdk/api/generated/default/models/TopUsersResponse.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -// @ts-nocheck -/** - * API - * Audius V1 API - * - * The version of the OpenAPI document: 1.0 - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - -import { exists, mapValues } from '../runtime'; -import type { User } from './User'; -import { - UserFromJSON, - UserFromJSONTyped, - UserToJSON, -} from './User'; - -/** - * - * @export - * @interface TopUsersResponse - */ -export interface TopUsersResponse { - /** - * - * @type {Array} - * @memberof TopUsersResponse - */ - data?: Array; -} - -/** - * Check if a given object implements the TopUsersResponse interface. - */ -export function instanceOfTopUsersResponse(value: object): boolean { - let isInstance = true; - - return isInstance; -} - -export function TopUsersResponseFromJSON(json: any): TopUsersResponse { - return TopUsersResponseFromJSONTyped(json, false); -} - -export function TopUsersResponseFromJSONTyped(json: any, ignoreDiscriminator: boolean): TopUsersResponse { - if ((json === undefined) || (json === null)) { - return json; - } - return { - - 'data': !exists(json, 'data') ? undefined : ((json['data'] as Array).map(UserFromJSON)), - }; -} - -export function TopUsersResponseToJSON(value?: TopUsersResponse | null): any { - if (value === undefined) { - return undefined; - } - if (value === null) { - return null; - } - return { - - 'data': value.data === undefined ? undefined : ((value.data as Array).map(UserToJSON)), - }; -} - diff --git a/packages/libs/src/sdk/api/generated/default/models/Track.ts b/packages/libs/src/sdk/api/generated/default/models/Track.ts index dca2e861160..806c954f61d 100644 --- a/packages/libs/src/sdk/api/generated/default/models/Track.ts +++ b/packages/libs/src/sdk/api/generated/default/models/Track.ts @@ -14,6 +14,18 @@ */ import { exists, mapValues } from '../runtime'; +import type { Access } from './Access'; +import { + AccessFromJSON, + AccessFromJSONTyped, + AccessToJSON, +} from './Access'; +import type { AccessGate } from './AccessGate'; +import { + AccessGateFromJSON, + AccessGateFromJSONTyped, + AccessGateToJSON, +} from './AccessGate'; import type { RemixParent } from './RemixParent'; import { RemixParentFromJSON, @@ -39,12 +51,24 @@ import { * @interface Track */ export interface Track { + /** + * Describes what access the given user has + * @type {Access} + * @memberof Track + */ + access?: Access; /** * * @type {TrackArtwork} * @memberof Track */ artwork?: TrackArtwork; + /** + * The blocknumber this track was last updated + * @type {number} + * @memberof Track + */ + blocknumber: number; /** * * @type {string} @@ -183,6 +207,30 @@ export interface Track { * @memberof Track */ playlistsContainingTrack?: Array; + /** + * Whether or not the owner has restricted streaming behind an access gate + * @type {boolean} + * @memberof Track + */ + isStreamGated?: boolean; + /** + * How to unlock stream access to the track + * @type {AccessGate} + * @memberof Track + */ + streamConditions?: AccessGate; + /** + * Whether or not the owner has restricted downloading behind an access gate + * @type {boolean} + * @memberof Track + */ + isDownloadGated?: boolean; + /** + * How to unlock the track download + * @type {AccessGate} + * @memberof Track + */ + downloadConditions?: AccessGate; } /** @@ -190,6 +238,7 @@ export interface Track { */ export function instanceOfTrack(value: object): boolean { let isInstance = true; + isInstance = isInstance && "blocknumber" in value; isInstance = isInstance && "id" in value; isInstance = isInstance && "repostCount" in value; isInstance = isInstance && "favoriteCount" in value; @@ -211,7 +260,9 @@ export function TrackFromJSONTyped(json: any, ignoreDiscriminator: boolean): Tra } return { + 'access': !exists(json, 'access') ? undefined : AccessFromJSON(json['access']), 'artwork': !exists(json, 'artwork') ? undefined : TrackArtworkFromJSON(json['artwork']), + 'blocknumber': json['blocknumber'], 'description': !exists(json, 'description') ? undefined : json['description'], 'genre': !exists(json, 'genre') ? undefined : json['genre'], 'id': json['id'], @@ -235,6 +286,10 @@ export function TrackFromJSONTyped(json: any, ignoreDiscriminator: boolean): Tra 'isStreamable': !exists(json, 'is_streamable') ? undefined : json['is_streamable'], 'ddexApp': !exists(json, 'ddex_app') ? undefined : json['ddex_app'], 'playlistsContainingTrack': !exists(json, 'playlists_containing_track') ? undefined : json['playlists_containing_track'], + 'isStreamGated': !exists(json, 'is_stream_gated') ? undefined : json['is_stream_gated'], + 'streamConditions': !exists(json, 'stream_conditions') ? undefined : AccessGateFromJSON(json['stream_conditions']), + 'isDownloadGated': !exists(json, 'is_download_gated') ? undefined : json['is_download_gated'], + 'downloadConditions': !exists(json, 'download_conditions') ? undefined : AccessGateFromJSON(json['download_conditions']), }; } @@ -247,7 +302,9 @@ export function TrackToJSON(value?: Track | null): any { } return { + 'access': AccessToJSON(value.access), 'artwork': TrackArtworkToJSON(value.artwork), + 'blocknumber': value.blocknumber, 'description': value.description, 'genre': value.genre, 'id': value.id, @@ -271,6 +328,10 @@ export function TrackToJSON(value?: Track | null): any { 'is_streamable': value.isStreamable, 'ddex_app': value.ddexApp, 'playlists_containing_track': value.playlistsContainingTrack, + 'is_stream_gated': value.isStreamGated, + 'stream_conditions': AccessGateToJSON(value.streamConditions), + 'is_download_gated': value.isDownloadGated, + 'download_conditions': AccessGateToJSON(value.downloadConditions), }; } diff --git a/packages/libs/src/sdk/api/generated/default/models/TxSignature.ts b/packages/libs/src/sdk/api/generated/default/models/TxSignature.ts deleted file mode 100644 index 70df03522d0..00000000000 --- a/packages/libs/src/sdk/api/generated/default/models/TxSignature.ts +++ /dev/null @@ -1,76 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -// @ts-nocheck -/** - * API - * Audius V1 API - * - * The version of the OpenAPI document: 1.0 - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - -import { exists, mapValues } from '../runtime'; -/** - * - * @export - * @interface TxSignature - */ -export interface TxSignature { - /** - * - * @type {string} - * @memberof TxSignature - */ - message: string; - /** - * - * @type {string} - * @memberof TxSignature - */ - signature: string; -} - -/** - * Check if a given object implements the TxSignature interface. - */ -export function instanceOfTxSignature(value: object): boolean { - let isInstance = true; - isInstance = isInstance && "message" in value; - isInstance = isInstance && "signature" in value; - - return isInstance; -} - -export function TxSignatureFromJSON(json: any): TxSignature { - return TxSignatureFromJSONTyped(json, false); -} - -export function TxSignatureFromJSONTyped(json: any, ignoreDiscriminator: boolean): TxSignature { - if ((json === undefined) || (json === null)) { - return json; - } - return { - - 'message': json['message'], - 'signature': json['signature'], - }; -} - -export function TxSignatureToJSON(value?: TxSignature | null): any { - if (value === undefined) { - return undefined; - } - if (value === null) { - return null; - } - return { - - 'message': value.message, - 'signature': value.signature, - }; -} - diff --git a/packages/libs/src/sdk/api/generated/default/models/UsdcGate.ts b/packages/libs/src/sdk/api/generated/default/models/UsdcGate.ts new file mode 100644 index 00000000000..4f08afe0d82 --- /dev/null +++ b/packages/libs/src/sdk/api/generated/default/models/UsdcGate.ts @@ -0,0 +1,76 @@ +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck +/** + * API + * Audius V1 API + * + * The version of the OpenAPI document: 1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { exists, mapValues } from '../runtime'; +/** + * + * @export + * @interface UsdcGate + */ +export interface UsdcGate { + /** + * + * @type {{ [key: string]: number; }} + * @memberof UsdcGate + */ + splits: { [key: string]: number; }; + /** + * + * @type {number} + * @memberof UsdcGate + */ + price: number; +} + +/** + * Check if a given object implements the UsdcGate interface. + */ +export function instanceOfUsdcGate(value: object): boolean { + let isInstance = true; + isInstance = isInstance && "splits" in value; + isInstance = isInstance && "price" in value; + + return isInstance; +} + +export function UsdcGateFromJSON(json: any): UsdcGate { + return UsdcGateFromJSONTyped(json, false); +} + +export function UsdcGateFromJSONTyped(json: any, ignoreDiscriminator: boolean): UsdcGate { + if ((json === undefined) || (json === null)) { + return json; + } + return { + + 'splits': json['splits'], + 'price': json['price'], + }; +} + +export function UsdcGateToJSON(value?: UsdcGate | null): any { + if (value === undefined) { + return undefined; + } + if (value === null) { + return null; + } + return { + + 'splits': value.splits, + 'price': value.price, + }; +} + diff --git a/packages/libs/src/sdk/api/generated/default/models/User.ts b/packages/libs/src/sdk/api/generated/default/models/User.ts index f0d4403c5d5..24e632b9d9c 100644 --- a/packages/libs/src/sdk/api/generated/default/models/User.ts +++ b/packages/libs/src/sdk/api/generated/default/models/User.ts @@ -165,6 +165,12 @@ export interface User { * @memberof User */ totalAudioBalance: number; + /** + * The user's Ethereum wallet address for their account + * @type {string} + * @memberof User + */ + wallet: string; } /** @@ -189,6 +195,7 @@ export function instanceOfUser(value: object): boolean { isInstance = isInstance && "supporterCount" in value; isInstance = isInstance && "supportingCount" in value; isInstance = isInstance && "totalAudioBalance" in value; + isInstance = isInstance && "wallet" in value; return isInstance; } @@ -225,6 +232,7 @@ export function UserFromJSONTyped(json: any, ignoreDiscriminator: boolean): User 'supporterCount': json['supporter_count'], 'supportingCount': json['supporting_count'], 'totalAudioBalance': json['total_audio_balance'], + 'wallet': json['wallet'], }; } @@ -259,6 +267,7 @@ export function UserToJSON(value?: User | null): any { 'supporter_count': value.supporterCount, 'supporting_count': value.supportingCount, 'total_audio_balance': value.totalAudioBalance, + 'wallet': value.wallet, }; } diff --git a/packages/libs/src/sdk/api/generated/default/models/UserReplicaSet.ts b/packages/libs/src/sdk/api/generated/default/models/UserReplicaSet.ts deleted file mode 100644 index cc6ba5500b7..00000000000 --- a/packages/libs/src/sdk/api/generated/default/models/UserReplicaSet.ts +++ /dev/null @@ -1,124 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -// @ts-nocheck -/** - * API - * Audius V1 API - * - * The version of the OpenAPI document: 1.0 - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - -import { exists, mapValues } from '../runtime'; -/** - * - * @export - * @interface UserReplicaSet - */ -export interface UserReplicaSet { - /** - * - * @type {number} - * @memberof UserReplicaSet - */ - userId: number; - /** - * - * @type {string} - * @memberof UserReplicaSet - */ - wallet: string; - /** - * - * @type {string} - * @memberof UserReplicaSet - */ - primary?: string; - /** - * - * @type {string} - * @memberof UserReplicaSet - */ - secondary1?: string; - /** - * - * @type {string} - * @memberof UserReplicaSet - */ - secondary2?: string; - /** - * - * @type {number} - * @memberof UserReplicaSet - */ - primarySpID?: number; - /** - * - * @type {number} - * @memberof UserReplicaSet - */ - secondary1SpID?: number; - /** - * - * @type {number} - * @memberof UserReplicaSet - */ - secondary2SpID?: number; -} - -/** - * Check if a given object implements the UserReplicaSet interface. - */ -export function instanceOfUserReplicaSet(value: object): boolean { - let isInstance = true; - isInstance = isInstance && "userId" in value; - isInstance = isInstance && "wallet" in value; - - return isInstance; -} - -export function UserReplicaSetFromJSON(json: any): UserReplicaSet { - return UserReplicaSetFromJSONTyped(json, false); -} - -export function UserReplicaSetFromJSONTyped(json: any, ignoreDiscriminator: boolean): UserReplicaSet { - if ((json === undefined) || (json === null)) { - return json; - } - return { - - 'userId': json['user_id'], - 'wallet': json['wallet'], - 'primary': !exists(json, 'primary') ? undefined : json['primary'], - 'secondary1': !exists(json, 'secondary1') ? undefined : json['secondary1'], - 'secondary2': !exists(json, 'secondary2') ? undefined : json['secondary2'], - 'primarySpID': !exists(json, 'primarySpID') ? undefined : json['primarySpID'], - 'secondary1SpID': !exists(json, 'secondary1SpID') ? undefined : json['secondary1SpID'], - 'secondary2SpID': !exists(json, 'secondary2SpID') ? undefined : json['secondary2SpID'], - }; -} - -export function UserReplicaSetToJSON(value?: UserReplicaSet | null): any { - if (value === undefined) { - return undefined; - } - if (value === null) { - return null; - } - return { - - 'user_id': value.userId, - 'wallet': value.wallet, - 'primary': value.primary, - 'secondary1': value.secondary1, - 'secondary2': value.secondary2, - 'primarySpID': value.primarySpID, - 'secondary1SpID': value.secondary1SpID, - 'secondary2SpID': value.secondary2SpID, - }; -} - diff --git a/packages/libs/src/sdk/api/generated/default/models/UserSubscribers.ts b/packages/libs/src/sdk/api/generated/default/models/UserSubscribers.ts deleted file mode 100644 index d4ef60cf589..00000000000 --- a/packages/libs/src/sdk/api/generated/default/models/UserSubscribers.ts +++ /dev/null @@ -1,75 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -// @ts-nocheck -/** - * API - * Audius V1 API - * - * The version of the OpenAPI document: 1.0 - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - -import { exists, mapValues } from '../runtime'; -/** - * - * @export - * @interface UserSubscribers - */ -export interface UserSubscribers { - /** - * - * @type {string} - * @memberof UserSubscribers - */ - userId: string; - /** - * - * @type {Array} - * @memberof UserSubscribers - */ - subscriberIds?: Array; -} - -/** - * Check if a given object implements the UserSubscribers interface. - */ -export function instanceOfUserSubscribers(value: object): boolean { - let isInstance = true; - isInstance = isInstance && "userId" in value; - - return isInstance; -} - -export function UserSubscribersFromJSON(json: any): UserSubscribers { - return UserSubscribersFromJSONTyped(json, false); -} - -export function UserSubscribersFromJSONTyped(json: any, ignoreDiscriminator: boolean): UserSubscribers { - if ((json === undefined) || (json === null)) { - return json; - } - return { - - 'userId': json['user_id'], - 'subscriberIds': !exists(json, 'subscriber_ids') ? undefined : json['subscriber_ids'], - }; -} - -export function UserSubscribersToJSON(value?: UserSubscribers | null): any { - if (value === undefined) { - return undefined; - } - if (value === null) { - return null; - } - return { - - 'user_id': value.userId, - 'subscriber_ids': value.subscriberIds, - }; -} - diff --git a/packages/libs/src/sdk/api/generated/default/models/UserTrackListenCountsResponse.ts b/packages/libs/src/sdk/api/generated/default/models/UserTrackListenCountsResponse.ts deleted file mode 100644 index e0aec6a9a41..00000000000 --- a/packages/libs/src/sdk/api/generated/default/models/UserTrackListenCountsResponse.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -// @ts-nocheck -/** - * API - * Audius V1 API - * - * The version of the OpenAPI document: 1.0 - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - -import { exists, mapValues } from '../runtime'; -import type { MonthlyAggregatePlay } from './MonthlyAggregatePlay'; -import { - MonthlyAggregatePlayFromJSON, - MonthlyAggregatePlayFromJSONTyped, - MonthlyAggregatePlayToJSON, -} from './MonthlyAggregatePlay'; - -/** - * - * @export - * @interface UserTrackListenCountsResponse - */ -export interface UserTrackListenCountsResponse { - /** - * - * @type {{ [key: string]: MonthlyAggregatePlay; }} - * @memberof UserTrackListenCountsResponse - */ - data?: { [key: string]: MonthlyAggregatePlay; }; -} - -/** - * Check if a given object implements the UserTrackListenCountsResponse interface. - */ -export function instanceOfUserTrackListenCountsResponse(value: object): boolean { - let isInstance = true; - - return isInstance; -} - -export function UserTrackListenCountsResponseFromJSON(json: any): UserTrackListenCountsResponse { - return UserTrackListenCountsResponseFromJSONTyped(json, false); -} - -export function UserTrackListenCountsResponseFromJSONTyped(json: any, ignoreDiscriminator: boolean): UserTrackListenCountsResponse { - if ((json === undefined) || (json === null)) { - return json; - } - return { - - 'data': !exists(json, 'data') ? undefined : (mapValues(json['data'], MonthlyAggregatePlayFromJSON)), - }; -} - -export function UserTrackListenCountsResponseToJSON(value?: UserTrackListenCountsResponse | null): any { - if (value === undefined) { - return undefined; - } - if (value === null) { - return null; - } - return { - - 'data': value.data === undefined ? undefined : (mapValues(value.data, MonthlyAggregatePlayToJSON)), - }; -} - diff --git a/packages/libs/src/sdk/api/generated/default/models/UsersByContentNode.ts b/packages/libs/src/sdk/api/generated/default/models/UsersByContentNode.ts deleted file mode 100644 index 1f3d1c763a6..00000000000 --- a/packages/libs/src/sdk/api/generated/default/models/UsersByContentNode.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -// @ts-nocheck -/** - * API - * Audius V1 API - * - * The version of the OpenAPI document: 1.0 - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - -import { exists, mapValues } from '../runtime'; -import type { UserReplicaSet } from './UserReplicaSet'; -import { - UserReplicaSetFromJSON, - UserReplicaSetFromJSONTyped, - UserReplicaSetToJSON, -} from './UserReplicaSet'; - -/** - * - * @export - * @interface UsersByContentNode - */ -export interface UsersByContentNode { - /** - * - * @type {UserReplicaSet} - * @memberof UsersByContentNode - */ - data?: UserReplicaSet; -} - -/** - * Check if a given object implements the UsersByContentNode interface. - */ -export function instanceOfUsersByContentNode(value: object): boolean { - let isInstance = true; - - return isInstance; -} - -export function UsersByContentNodeFromJSON(json: any): UsersByContentNode { - return UsersByContentNodeFromJSONTyped(json, false); -} - -export function UsersByContentNodeFromJSONTyped(json: any, ignoreDiscriminator: boolean): UsersByContentNode { - if ((json === undefined) || (json === null)) { - return json; - } - return { - - 'data': !exists(json, 'data') ? undefined : UserReplicaSetFromJSON(json['data']), - }; -} - -export function UsersByContentNodeToJSON(value?: UsersByContentNode | null): any { - if (value === undefined) { - return undefined; - } - if (value === null) { - return null; - } - return { - - 'data': UserReplicaSetToJSON(value.data), - }; -} - diff --git a/packages/libs/src/sdk/api/generated/default/models/VersionMetadata.ts b/packages/libs/src/sdk/api/generated/default/models/VersionMetadata.ts deleted file mode 100644 index 02932359460..00000000000 --- a/packages/libs/src/sdk/api/generated/default/models/VersionMetadata.ts +++ /dev/null @@ -1,76 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -// @ts-nocheck -/** - * API - * Audius V1 API - * - * The version of the OpenAPI document: 1.0 - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - -import { exists, mapValues } from '../runtime'; -/** - * - * @export - * @interface VersionMetadata - */ -export interface VersionMetadata { - /** - * - * @type {string} - * @memberof VersionMetadata - */ - service: string; - /** - * - * @type {string} - * @memberof VersionMetadata - */ - version: string; -} - -/** - * Check if a given object implements the VersionMetadata interface. - */ -export function instanceOfVersionMetadata(value: object): boolean { - let isInstance = true; - isInstance = isInstance && "service" in value; - isInstance = isInstance && "version" in value; - - return isInstance; -} - -export function VersionMetadataFromJSON(json: any): VersionMetadata { - return VersionMetadataFromJSONTyped(json, false); -} - -export function VersionMetadataFromJSONTyped(json: any, ignoreDiscriminator: boolean): VersionMetadata { - if ((json === undefined) || (json === null)) { - return json; - } - return { - - 'service': json['service'], - 'version': json['version'], - }; -} - -export function VersionMetadataToJSON(value?: VersionMetadata | null): any { - if (value === undefined) { - return undefined; - } - if (value === null) { - return null; - } - return { - - 'service': value.service, - 'version': value.version, - }; -} - diff --git a/packages/libs/src/sdk/api/generated/default/models/index.ts b/packages/libs/src/sdk/api/generated/default/models/index.ts index 4f2ce30344a..0afefa82f0f 100644 --- a/packages/libs/src/sdk/api/generated/default/models/index.ts +++ b/packages/libs/src/sdk/api/generated/default/models/index.ts @@ -1,6 +1,7 @@ /* tslint:disable */ /* eslint-disable */ export * from './Access'; +export * from './AccessGate'; export * from './Activity'; export * from './AuthorizedApp'; export * from './AuthorizedApps'; @@ -17,11 +18,14 @@ export * from './DeveloperApps'; export * from './EncodedUserId'; export * from './Favorite'; export * from './FavoritesResponse'; +export * from './FollowGate'; export * from './FollowersResponse'; export * from './FollowingResponse'; export * from './GetSupporters'; export * from './GetSupporting'; export * from './GetTipsResponse'; +export * from './NftCollection'; +export * from './NftGate'; export * from './Playlist'; export * from './PlaylistAddedTimestamp'; export * from './PlaylistArtwork'; @@ -29,6 +33,7 @@ export * from './PlaylistResponse'; export * from './PlaylistSearchResult'; export * from './PlaylistTracksResponse'; export * from './ProfilePicture'; +export * from './PurchaseGate'; export * from './RelatedArtistResponse'; export * from './RemixParent'; export * from './Reposts'; @@ -37,6 +42,7 @@ export * from './Supporter'; export * from './Supporting'; export * from './TagsResponse'; export * from './Tip'; +export * from './TipGate'; export * from './TopListener'; export * from './Track'; export * from './TrackArtwork'; @@ -46,6 +52,7 @@ export * from './TrackResponse'; export * from './TrackSearch'; export * from './TracksResponse'; export * from './TrendingPlaylistsResponse'; +export * from './UsdcGate'; export * from './User'; export * from './UserAssociatedWalletResponse'; export * from './UserResponse'; diff --git a/packages/libs/src/sdk/api/generated/full/.openapi-generator/VERSION b/packages/libs/src/sdk/api/generated/full/.openapi-generator/VERSION index 08bfd0643b8..18bb4182dd0 100644 --- a/packages/libs/src/sdk/api/generated/full/.openapi-generator/VERSION +++ b/packages/libs/src/sdk/api/generated/full/.openapi-generator/VERSION @@ -1 +1 @@ -7.5.0-SNAPSHOT +7.5.0 diff --git a/packages/libs/src/sdk/api/generated/full/models/FollowGate.ts b/packages/libs/src/sdk/api/generated/full/models/FollowGate.ts index d337da812c3..8b066fc5bd8 100644 --- a/packages/libs/src/sdk/api/generated/full/models/FollowGate.ts +++ b/packages/libs/src/sdk/api/generated/full/models/FollowGate.ts @@ -21,7 +21,7 @@ import { exists, mapValues } from '../runtime'; */ export interface FollowGate { /** - * + * Must follow the given user ID to unlock * @type {number} * @memberof FollowGate */ diff --git a/packages/libs/src/sdk/api/generated/full/models/NftGate.ts b/packages/libs/src/sdk/api/generated/full/models/NftGate.ts index 247575bfbef..a014be6660f 100644 --- a/packages/libs/src/sdk/api/generated/full/models/NftGate.ts +++ b/packages/libs/src/sdk/api/generated/full/models/NftGate.ts @@ -28,7 +28,7 @@ import { */ export interface NftGate { /** - * + * Must hold an NFT of the given collection to unlock * @type {NftCollection} * @memberof NftGate */ diff --git a/packages/libs/src/sdk/api/generated/full/models/PurchaseGate.ts b/packages/libs/src/sdk/api/generated/full/models/PurchaseGate.ts index 6bff7ad4a1f..b9ecc62681d 100644 --- a/packages/libs/src/sdk/api/generated/full/models/PurchaseGate.ts +++ b/packages/libs/src/sdk/api/generated/full/models/PurchaseGate.ts @@ -28,7 +28,7 @@ import { */ export interface PurchaseGate { /** - * + * Must pay the total price and split to the given addresses to unlock * @type {UsdcGate} * @memberof PurchaseGate */ diff --git a/packages/libs/src/sdk/api/generated/full/models/TipGate.ts b/packages/libs/src/sdk/api/generated/full/models/TipGate.ts index 97bd779f3e8..a8c8649758e 100644 --- a/packages/libs/src/sdk/api/generated/full/models/TipGate.ts +++ b/packages/libs/src/sdk/api/generated/full/models/TipGate.ts @@ -21,7 +21,7 @@ import { exists, mapValues } from '../runtime'; */ export interface TipGate { /** - * + * Must tip the given user ID to unlock * @type {number} * @memberof TipGate */ diff --git a/packages/libs/src/sdk/api/generated/full/models/TrackFull.ts b/packages/libs/src/sdk/api/generated/full/models/TrackFull.ts index 8c479aec398..f2256dae62c 100644 --- a/packages/libs/src/sdk/api/generated/full/models/TrackFull.ts +++ b/packages/libs/src/sdk/api/generated/full/models/TrackFull.ts @@ -93,12 +93,24 @@ import { * @interface TrackFull */ export interface TrackFull { + /** + * Describes what access the given user has + * @type {Access} + * @memberof TrackFull + */ + access?: Access; /** * * @type {TrackArtwork} * @memberof TrackFull */ artwork?: TrackArtwork; + /** + * The blocknumber this track was last updated + * @type {number} + * @memberof TrackFull + */ + blocknumber: number; /** * * @type {string} @@ -238,11 +250,29 @@ export interface TrackFull { */ playlistsContainingTrack?: Array; /** - * - * @type {number} + * Whether or not the owner has restricted streaming behind an access gate + * @type {boolean} * @memberof TrackFull */ - blocknumber: number; + isStreamGated?: boolean; + /** + * How to unlock stream access to the track + * @type {AccessGate} + * @memberof TrackFull + */ + streamConditions?: AccessGate; + /** + * Whether or not the owner has restricted downloading behind an access gate + * @type {boolean} + * @memberof TrackFull + */ + isDownloadGated?: boolean; + /** + * How to unlock the track download + * @type {AccessGate} + * @memberof TrackFull + */ + downloadConditions?: AccessGate; /** * * @type {string} @@ -387,36 +417,6 @@ export interface TrackFull { * @memberof TrackFull */ isAvailable?: boolean; - /** - * - * @type {boolean} - * @memberof TrackFull - */ - isStreamGated?: boolean; - /** - * - * @type {AccessGate} - * @memberof TrackFull - */ - streamConditions?: AccessGate; - /** - * - * @type {boolean} - * @memberof TrackFull - */ - isDownloadGated?: boolean; - /** - * - * @type {AccessGate} - * @memberof TrackFull - */ - downloadConditions?: AccessGate; - /** - * - * @type {Access} - * @memberof TrackFull - */ - access?: Access; /** * * @type {number} @@ -484,6 +484,7 @@ export interface TrackFull { */ export function instanceOfTrackFull(value: object): boolean { let isInstance = true; + isInstance = isInstance && "blocknumber" in value; isInstance = isInstance && "id" in value; isInstance = isInstance && "repostCount" in value; isInstance = isInstance && "favoriteCount" in value; @@ -491,7 +492,6 @@ export function instanceOfTrackFull(value: object): boolean { isInstance = isInstance && "user" in value; isInstance = isInstance && "duration" in value; isInstance = isInstance && "playCount" in value; - isInstance = isInstance && "blocknumber" in value; isInstance = isInstance && "followeeReposts" in value; isInstance = isInstance && "hasCurrentUserReposted" in value; isInstance = isInstance && "isUnlisted" in value; @@ -513,7 +513,9 @@ export function TrackFullFromJSONTyped(json: any, ignoreDiscriminator: boolean): } return { + 'access': !exists(json, 'access') ? undefined : AccessFromJSON(json['access']), 'artwork': !exists(json, 'artwork') ? undefined : TrackArtworkFromJSON(json['artwork']), + 'blocknumber': json['blocknumber'], 'description': !exists(json, 'description') ? undefined : json['description'], 'genre': !exists(json, 'genre') ? undefined : json['genre'], 'id': json['id'], @@ -537,7 +539,10 @@ export function TrackFullFromJSONTyped(json: any, ignoreDiscriminator: boolean): 'isStreamable': !exists(json, 'is_streamable') ? undefined : json['is_streamable'], 'ddexApp': !exists(json, 'ddex_app') ? undefined : json['ddex_app'], 'playlistsContainingTrack': !exists(json, 'playlists_containing_track') ? undefined : json['playlists_containing_track'], - 'blocknumber': json['blocknumber'], + 'isStreamGated': !exists(json, 'is_stream_gated') ? undefined : json['is_stream_gated'], + 'streamConditions': !exists(json, 'stream_conditions') ? undefined : AccessGateFromJSON(json['stream_conditions']), + 'isDownloadGated': !exists(json, 'is_download_gated') ? undefined : json['is_download_gated'], + 'downloadConditions': !exists(json, 'download_conditions') ? undefined : AccessGateFromJSON(json['download_conditions']), 'createDate': !exists(json, 'create_date') ? undefined : json['create_date'], 'coverArtSizes': !exists(json, 'cover_art_sizes') ? undefined : json['cover_art_sizes'], 'coverArtCids': !exists(json, 'cover_art_cids') ? undefined : CoverArtFromJSON(json['cover_art_cids']), @@ -562,11 +567,6 @@ export function TrackFullFromJSONTyped(json: any, ignoreDiscriminator: boolean): 'isDelete': !exists(json, 'is_delete') ? undefined : json['is_delete'], 'coverArt': !exists(json, 'cover_art') ? undefined : json['cover_art'], 'isAvailable': !exists(json, 'is_available') ? undefined : json['is_available'], - 'isStreamGated': !exists(json, 'is_stream_gated') ? undefined : json['is_stream_gated'], - 'streamConditions': !exists(json, 'stream_conditions') ? undefined : AccessGateFromJSON(json['stream_conditions']), - 'isDownloadGated': !exists(json, 'is_download_gated') ? undefined : json['is_download_gated'], - 'downloadConditions': !exists(json, 'download_conditions') ? undefined : AccessGateFromJSON(json['download_conditions']), - 'access': !exists(json, 'access') ? undefined : AccessFromJSON(json['access']), 'aiAttributionUserId': !exists(json, 'ai_attribution_user_id') ? undefined : json['ai_attribution_user_id'], 'audioUploadId': !exists(json, 'audio_upload_id') ? undefined : json['audio_upload_id'], 'previewStartSeconds': !exists(json, 'preview_start_seconds') ? undefined : json['preview_start_seconds'], @@ -589,7 +589,9 @@ export function TrackFullToJSON(value?: TrackFull | null): any { } return { + 'access': AccessToJSON(value.access), 'artwork': TrackArtworkToJSON(value.artwork), + 'blocknumber': value.blocknumber, 'description': value.description, 'genre': value.genre, 'id': value.id, @@ -613,7 +615,10 @@ export function TrackFullToJSON(value?: TrackFull | null): any { 'is_streamable': value.isStreamable, 'ddex_app': value.ddexApp, 'playlists_containing_track': value.playlistsContainingTrack, - 'blocknumber': value.blocknumber, + 'is_stream_gated': value.isStreamGated, + 'stream_conditions': AccessGateToJSON(value.streamConditions), + 'is_download_gated': value.isDownloadGated, + 'download_conditions': AccessGateToJSON(value.downloadConditions), 'create_date': value.createDate, 'cover_art_sizes': value.coverArtSizes, 'cover_art_cids': CoverArtToJSON(value.coverArtCids), @@ -638,11 +643,6 @@ export function TrackFullToJSON(value?: TrackFull | null): any { 'is_delete': value.isDelete, 'cover_art': value.coverArt, 'is_available': value.isAvailable, - 'is_stream_gated': value.isStreamGated, - 'stream_conditions': AccessGateToJSON(value.streamConditions), - 'is_download_gated': value.isDownloadGated, - 'download_conditions': AccessGateToJSON(value.downloadConditions), - 'access': AccessToJSON(value.access), 'ai_attribution_user_id': value.aiAttributionUserId, 'audio_upload_id': value.audioUploadId, 'preview_start_seconds': value.previewStartSeconds, diff --git a/packages/libs/src/sdk/api/generated/full/models/UserFull.ts b/packages/libs/src/sdk/api/generated/full/models/UserFull.ts index 05fec28b1b5..a676215ee14 100644 --- a/packages/libs/src/sdk/api/generated/full/models/UserFull.ts +++ b/packages/libs/src/sdk/api/generated/full/models/UserFull.ts @@ -171,6 +171,12 @@ export interface UserFull { * @memberof UserFull */ totalAudioBalance: number; + /** + * The user's Ethereum wallet address for their account + * @type {string} + * @memberof UserFull + */ + wallet: string; /** * * @type {string} @@ -207,12 +213,6 @@ export interface UserFull { * @memberof UserFull */ blocknumber: number; - /** - * - * @type {string} - * @memberof UserFull - */ - wallet: string; /** * * @type {string} @@ -345,13 +345,13 @@ export function instanceOfUserFull(value: object): boolean { isInstance = isInstance && "supporterCount" in value; isInstance = isInstance && "supportingCount" in value; isInstance = isInstance && "totalAudioBalance" in value; + isInstance = isInstance && "wallet" in value; isInstance = isInstance && "balance" in value; isInstance = isInstance && "associatedWalletsBalance" in value; isInstance = isInstance && "totalBalance" in value; isInstance = isInstance && "waudioBalance" in value; isInstance = isInstance && "associatedSolWalletsBalance" in value; isInstance = isInstance && "blocknumber" in value; - isInstance = isInstance && "wallet" in value; isInstance = isInstance && "createdAt" in value; isInstance = isInstance && "isStorageV2" in value; isInstance = isInstance && "currentUserFolloweeFollowCount" in value; @@ -397,13 +397,13 @@ export function UserFullFromJSONTyped(json: any, ignoreDiscriminator: boolean): 'supporterCount': json['supporter_count'], 'supportingCount': json['supporting_count'], 'totalAudioBalance': json['total_audio_balance'], + 'wallet': json['wallet'], 'balance': json['balance'], 'associatedWalletsBalance': json['associated_wallets_balance'], 'totalBalance': json['total_balance'], 'waudioBalance': json['waudio_balance'], 'associatedSolWalletsBalance': json['associated_sol_wallets_balance'], 'blocknumber': json['blocknumber'], - 'wallet': json['wallet'], 'createdAt': json['created_at'], 'isStorageV2': json['is_storage_v2'], 'creatorNodeEndpoint': !exists(json, 'creator_node_endpoint') ? undefined : json['creator_node_endpoint'], @@ -456,13 +456,13 @@ export function UserFullToJSON(value?: UserFull | null): any { 'supporter_count': value.supporterCount, 'supporting_count': value.supportingCount, 'total_audio_balance': value.totalAudioBalance, + 'wallet': value.wallet, 'balance': value.balance, 'associated_wallets_balance': value.associatedWalletsBalance, 'total_balance': value.totalBalance, 'waudio_balance': value.waudioBalance, 'associated_sol_wallets_balance': value.associatedSolWalletsBalance, 'blocknumber': value.blocknumber, - 'wallet': value.wallet, 'created_at': value.createdAt, 'is_storage_v2': value.isStorageV2, 'creator_node_endpoint': value.creatorNodeEndpoint, diff --git a/packages/libs/src/sdk/api/generator/gen.js b/packages/libs/src/sdk/api/generator/gen.js index 47413f1bea3..ec5d77dcb46 100644 --- a/packages/libs/src/sdk/api/generator/gen.js +++ b/packages/libs/src/sdk/api/generator/gen.js @@ -22,9 +22,9 @@ const GENERATED_DIR = 'src/sdk/api/generated' const spawnOpenAPIGenerator = async (openApiGeneratorArgs) => { console.info('Running OpenAPI Generator:') - const fullCmd = `docker run --add-host=audius-protocol-discovery-provider-1:host-gateway --rm -v "${ + const fullCmd = `docker run --add-host=audius-protocol-discovery-provider-1:host-gateway --user $(id -u):$(id -g) --rm -v "${ process.env.PWD - }:/local" openapitools/openapi-generator-cli ${openApiGeneratorArgs.join( + }:/local" openapitools/openapi-generator-cli:v7.5.0 ${openApiGeneratorArgs.join( ' ' )}` console.info(fullCmd) diff --git a/packages/libs/src/sdk/api/tracks/TracksApi.test.ts b/packages/libs/src/sdk/api/tracks/TracksApi.test.ts index 22cfaf663f1..cc790162bed 100644 --- a/packages/libs/src/sdk/api/tracks/TracksApi.test.ts +++ b/packages/libs/src/sdk/api/tracks/TracksApi.test.ts @@ -3,10 +3,21 @@ import path from 'path' import { beforeAll, expect, jest } from '@jest/globals' +import { developmentConfig } from '../../config/development' +import { + PaymentRouterClient, + SolanaRelay, + SolanaRelayWalletAdapter, + getDefaultPaymentRouterClientConfig +} from '../../services' import { DefaultAuth } from '../../services/Auth/DefaultAuth' import { DiscoveryNodeSelector } from '../../services/DiscoveryNodeSelector' import { EntityManager } from '../../services/EntityManager' import { Logger } from '../../services/Logger' +import { + ClaimableTokensClient, + getDefaultClaimableTokensConfig +} from '../../services/Solana/programs/ClaimableTokensClient' import { Storage } from '../../services/Storage' import { StorageNodeSelector } from '../../services/StorageNodeSelector' import { Genre } from '../../types/Genre' @@ -83,13 +94,28 @@ describe('TracksApi', () => { }) beforeAll(() => { + const solanaWalletAdapter = new SolanaRelayWalletAdapter({ + solanaRelay: new SolanaRelay( + new Configuration({ + middleware: [discoveryNodeSelector.createMiddleware()] + }) + ) + }) tracks = new TracksApi( new Configuration(), new DiscoveryNodeSelector(), new Storage({ storageNodeSelector, logger: new Logger() }), new EntityManager({ discoveryNodeSelector: new DiscoveryNodeSelector() }), auth, - new Logger() + new Logger(), + new ClaimableTokensClient({ + ...getDefaultClaimableTokensConfig(developmentConfig), + solanaWalletAdapter + }), + new PaymentRouterClient({ + ...getDefaultPaymentRouterClientConfig(developmentConfig), + solanaWalletAdapter + }) ) jest.spyOn(console, 'warn').mockImplementation(() => {}) jest.spyOn(console, 'info').mockImplementation(() => {}) diff --git a/packages/libs/src/sdk/api/tracks/TracksApi.ts b/packages/libs/src/sdk/api/tracks/TracksApi.ts index df3dfe6fc53..447715cad38 100644 --- a/packages/libs/src/sdk/api/tracks/TracksApi.ts +++ b/packages/libs/src/sdk/api/tracks/TracksApi.ts @@ -1,6 +1,12 @@ +import { USDC } from '@audius/fixed-decimal' import snakecaseKeys from 'snakecase-keys' -import type { EntityManagerService, AuthService } from '../../services' +import type { + EntityManagerService, + AuthService, + ClaimableTokensClient, + PaymentRouterClient +} from '../../services' import type { DiscoveryNodeSelectorService } from '../../services/DiscoveryNodeSelector' import { Action, @@ -15,7 +21,8 @@ import { retry3 } from '../../utils/retry' import { Configuration, StreamTrackRequest, - TracksApi as GeneratedTracksApi + TracksApi as GeneratedTracksApi, + UsdcGate } from '../generated/default' import { BASE_PATH, RequiredError } from '../generated/default/runtime' @@ -34,7 +41,9 @@ import { UnfavoriteTrackRequest, UnfavoriteTrackSchema, UpdateTrackRequest, - UploadTrackRequest + UploadTrackRequest, + PurchaseTrackRequest, + PurchaseTrackSchema } from './types' // Extend that new class @@ -47,7 +56,9 @@ export class TracksApi extends GeneratedTracksApi { private readonly storage: StorageService, private readonly entityManager: EntityManagerService, private readonly auth: AuthService, - private readonly logger: LoggerService + private readonly logger: LoggerService, + private readonly claimableTokensClient: ClaimableTokensClient, + private readonly paymentRouterClient: PaymentRouterClient ) { super(configuration) this.trackUploadHelper = new TrackUploadHelper(configuration) @@ -366,4 +377,180 @@ export class TracksApi extends GeneratedTracksApi { ...advancedOptions }) } + + /** + * Purchases stream or download access to a track + * + * @hidden + */ + public async purchase(params: PurchaseTrackRequest) { + const { + userId, + trackId, + extraAmount: extraAmountNumber = 0, + walletAdapter + } = await parseParams('purchase', PurchaseTrackSchema)(params) + + const contentType = 'track' + const mint = 'USDC' + + // Fetch track + this.logger.debug('Fetching track...', { trackId }) + const { data: track } = await this.getTrack({ + trackId: params.trackId // use hashed trackId + }) + + // Validate purchase attempt + if (!track) { + throw new Error('Track not found.') + } + + if (!track.isStreamGated && !track.isDownloadGated) { + throw new Error('Attempted to purchase free track.') + } + + if (track.user.id === params.userId) { + throw new Error('Attempted to purchase own track.') + } + + let numberSplits: UsdcGate['splits'] = {} + let centPrice: number + let accessType: 'stream' | 'download' = 'stream' + + // Get conditions + if (track.streamConditions && 'usdcPurchase' in track.streamConditions) { + centPrice = track.streamConditions.usdcPurchase.price + numberSplits = track.streamConditions.usdcPurchase.splits + } else if ( + track.downloadConditions && + 'usdcPurchase' in track.downloadConditions + ) { + centPrice = track.downloadConditions.usdcPurchase.price + numberSplits = track.downloadConditions.usdcPurchase.splits + accessType = 'download' + } else { + throw new Error('Track is not available for purchase.') + } + + // Check if already purchased + if ( + (accessType === 'download' && track.access?.download) || + (accessType === 'stream' && track.access?.stream) + ) { + throw new Error('Track already purchased') + } + + let extraAmount = USDC(extraAmountNumber).value + const total = USDC(centPrice / 100.0).value + extraAmount + this.logger.debug('Purchase total:', total) + + // Convert splits to big int and spread extra amount to every split + const splits = Object.entries(numberSplits).reduce( + (prev, [key, value], index, arr) => { + const amountToAdd = extraAmount / BigInt(arr.length - index) + extraAmount = USDC(extraAmount - amountToAdd).value + return { + ...prev, + [key]: BigInt(value) + amountToAdd + } + }, + {} + ) + this.logger.debug('Calculated splits after extra amount:', splits) + + // Create user bank for recipient if not exists + this.logger.debug('Checking for recipient user bank...') + const { userBank: recipientUserBank, didExist } = + await this.claimableTokensClient.getOrCreateUserBank({ + ethWallet: track.user.wallet, + mint: 'USDC' + }) + if (!didExist) { + this.logger.debug('Created user bank', { + recipientUserBank: recipientUserBank.toBase58() + }) + } else { + this.logger.debug('User bank exists', { + recipientUserBank: recipientUserBank.toBase58() + }) + } + + const routeInstruction = + await this.paymentRouterClient.createRouteInstruction({ + splits, + total, + mint + }) + const memoInstruction = + await this.paymentRouterClient.createPurchaseMemoInstruction({ + contentId: trackId, + contentType, + blockNumber: track.blocknumber, + buyerUserId: userId, + accessType + }) + + if (walletAdapter) { + this.logger.debug( + `Using walletAdapter ${walletAdapter.name} to purchase...` + ) + if (!walletAdapter.connected) { + await walletAdapter.connect() + } + if (!walletAdapter.publicKey) { + throw new Error('Could not get connected wallet address') + } + // Use the specified Solana wallet + const transferInstruction = + await this.paymentRouterClient.createTransferInstruction({ + sourceWallet: walletAdapter.publicKey, + amount: total, + mint + }) + const transaction = await this.paymentRouterClient.buildTransaction({ + instructions: [transferInstruction, routeInstruction, memoInstruction] + }) + return await walletAdapter.sendTransaction( + transaction, + this.paymentRouterClient.connection + ) + } else { + // Use the authed wallet's userbank and relay + const ethWallet = await this.auth.getAddress() + this.logger.debug( + `Using userBank ${await this.claimableTokensClient.deriveUserBank({ + ethWallet, + mint: 'USDC' + })} to purchase...` + ) + const paymentRouterTokenAccount = + await this.paymentRouterClient.getOrCreateProgramTokenAccount({ + mint + }) + + const transferSecpInstruction = + await this.claimableTokensClient.createTransferSecpInstruction({ + ethWallet, + destination: paymentRouterTokenAccount.address, + mint, + amount: total, + auth: this.auth + }) + const transferInstruction = + await this.claimableTokensClient.createTransferInstruction({ + ethWallet, + destination: paymentRouterTokenAccount.address, + mint + }) + const transaction = await this.paymentRouterClient.buildTransaction({ + instructions: [ + transferSecpInstruction, + transferInstruction, + routeInstruction, + memoInstruction + ] + }) + return await this.paymentRouterClient.sendTransaction(transaction) + } + } } diff --git a/packages/libs/src/sdk/api/tracks/types.ts b/packages/libs/src/sdk/api/tracks/types.ts index 92e3446fc2e..3903f372b79 100644 --- a/packages/libs/src/sdk/api/tracks/types.ts +++ b/packages/libs/src/sdk/api/tracks/types.ts @@ -1,3 +1,4 @@ +import { BaseSignerWalletAdapter } from '@solana/wallet-adapter-base' import { z } from 'zod' import { @@ -267,3 +268,16 @@ export const UnrepostTrackSchema = z .strict() export type UnrepostTrackRequest = z.input + +export const PurchaseTrackSchema = z + .object({ + userId: HashId, + trackId: HashId, + extraAmount: z + .union([z.number().min(0), z.bigint().min(BigInt(0))]) + .optional(), + walletAdapter: z.instanceof(BaseSignerWalletAdapter).optional() + }) + .strict() + +export type PurchaseTrackRequest = z.input diff --git a/packages/libs/src/sdk/api/users/UsersApi.test.ts b/packages/libs/src/sdk/api/users/UsersApi.test.ts index 2a3edd6ce29..3d8526c5322 100644 --- a/packages/libs/src/sdk/api/users/UsersApi.test.ts +++ b/packages/libs/src/sdk/api/users/UsersApi.test.ts @@ -246,7 +246,8 @@ describe('UsersApi', () => { }) }) - describe('sendTip', () => { + // TODO: PAY-2911 + describe.skip('sendTip', () => { it('creates and relays a tip transaction with properly formed instructions', async () => { const senderUserId = '7eP5n' const receiverUserId = 'ML51L' diff --git a/packages/libs/src/sdk/config/development.ts b/packages/libs/src/sdk/config/development.ts index a2382c8e9f1..3efa8678330 100644 --- a/packages/libs/src/sdk/config/development.ts +++ b/packages/libs/src/sdk/config/development.ts @@ -38,6 +38,7 @@ export const developmentConfig: SdkServicesConfig = { "claimableTokensProgramAddress": "testHKV1B56fbvop4w6f2cTGEub9dRQ2Euta5VmqdX9", "rewardManagerProgramAddress": "testLsJKtyABc9UXJF8JWFKf1YH4LmqCWBC42c6akPb", "rewardManagerStateAddress": "DJPzVothq58SmkpRb1ATn5ddN2Rpv1j2TcGvM3XsHf1c", + "paymentRouterProgramAddress": "apaySbqV1XAmuiGszeN4NyWrXkkMrnuJVoNhzmS1AMa", "rpcEndpoint": "http://audius-protocol-solana-test-validator-1", "usdcTokenMint": "26Q7gP8UfkDzi7GMFEQxTJaNJ8D2ybCUjex58M5MLu8y", "wAudioTokenMint": "37RCjhgV1qGV2Q54EHFScdxZ22ydRMdKMtVgod47fDP3" diff --git a/packages/libs/src/sdk/config/production.ts b/packages/libs/src/sdk/config/production.ts index 15b1afe96d6..32c5a339878 100644 --- a/packages/libs/src/sdk/config/production.ts +++ b/packages/libs/src/sdk/config/production.ts @@ -609,6 +609,7 @@ export const productionConfig: SdkServicesConfig = { "claimableTokensProgramAddress": "Ewkv3JahEFRKkcJmpoKB7pXbnUHwjAyXiwEo4ZY2rezQ", "rewardManagerProgramAddress": "DDZDcYdQFEMwcu2Mwo75yGFjJ1mUQyyXLWzhZLEVFcei", "rewardManagerStateAddress": "71hWFVYokLaN1PNYzTAWi13EfJ7Xt9VbSWUKsXUT8mxE", + "paymentRouterProgramAddress": "paytYpX3LPN98TAeen6bFFeraGSuWnomZmCXjAsoqPa", "rpcEndpoint": "https://audius-fe.rpcpool.com", "usdcTokenMint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", "wAudioTokenMint": "9LzCMqDgTKYz9Drzqnpgee3SGa89up3a247ypMj2xrqM" diff --git a/packages/libs/src/sdk/config/staging.ts b/packages/libs/src/sdk/config/staging.ts index df3831cdebc..200f235ea8b 100644 --- a/packages/libs/src/sdk/config/staging.ts +++ b/packages/libs/src/sdk/config/staging.ts @@ -83,6 +83,7 @@ export const stagingConfig: SdkServicesConfig = { "claimableTokensProgramAddress": "2sjQNmUfkV6yKKi4dPR8gWRgtyma5aiymE3aXL2RAZww", "rewardManagerProgramAddress": "CDpzvz7DfgbF95jSSCHLX3ERkugyfgn9Fw8ypNZ1hfXp", "rewardManagerStateAddress": "GaiG9LDYHfZGqeNaoGRzFEnLiwUT7WiC6sA6FDJX9ZPq", + "paymentRouterProgramAddress": "sp38CXGL9FoWPp9Avo4fevewEX4UqNkTSTFUPpQFRry", "rpcEndpoint": "https://audius-fe.rpcpool.com", "usdcTokenMint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", "wAudioTokenMint": "BELGiMZQ34SDE6x2FUaML2UHDAgBLS64xvhXjX5tBBZo" diff --git a/packages/libs/src/sdk/config/types.ts b/packages/libs/src/sdk/config/types.ts index bd1819b530d..26782b1c744 100644 --- a/packages/libs/src/sdk/config/types.ts +++ b/packages/libs/src/sdk/config/types.ts @@ -19,6 +19,7 @@ export type SdkServicesConfig = { claimableTokensProgramAddress: string rewardManagerProgramAddress: string rewardManagerStateAddress: string + paymentRouterProgramAddress: string rpcEndpoint: string usdcTokenMint: string wAudioTokenMint: string diff --git a/packages/libs/src/sdk/scripts/generateServicesConfig.ts b/packages/libs/src/sdk/scripts/generateServicesConfig.ts index b9809e48045..cffb6cff478 100644 --- a/packages/libs/src/sdk/scripts/generateServicesConfig.ts +++ b/packages/libs/src/sdk/scripts/generateServicesConfig.ts @@ -80,6 +80,7 @@ const productionConfig: SdkServicesConfig = { 'Ewkv3JahEFRKkcJmpoKB7pXbnUHwjAyXiwEo4ZY2rezQ', rewardManagerProgramAddress: 'DDZDcYdQFEMwcu2Mwo75yGFjJ1mUQyyXLWzhZLEVFcei', rewardManagerStateAddress: '71hWFVYokLaN1PNYzTAWi13EfJ7Xt9VbSWUKsXUT8mxE', + paymentRouterProgramAddress: 'paytYpX3LPN98TAeen6bFFeraGSuWnomZmCXjAsoqPa', rpcEndpoint: 'https://audius-fe.rpcpool.com', usdcTokenMint: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', wAudioTokenMint: '9LzCMqDgTKYz9Drzqnpgee3SGa89up3a247ypMj2xrqM' @@ -106,6 +107,7 @@ const stagingConfig: SdkServicesConfig = { '2sjQNmUfkV6yKKi4dPR8gWRgtyma5aiymE3aXL2RAZww', rewardManagerProgramAddress: 'CDpzvz7DfgbF95jSSCHLX3ERkugyfgn9Fw8ypNZ1hfXp', rewardManagerStateAddress: 'GaiG9LDYHfZGqeNaoGRzFEnLiwUT7WiC6sA6FDJX9ZPq', + paymentRouterProgramAddress: 'sp38CXGL9FoWPp9Avo4fevewEX4UqNkTSTFUPpQFRry', rpcEndpoint: 'https://audius-fe.rpcpool.com', usdcTokenMint: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', wAudioTokenMint: 'BELGiMZQ34SDE6x2FUaML2UHDAgBLS64xvhXjX5tBBZo' @@ -145,6 +147,7 @@ const developmentConfig: SdkServicesConfig = { 'testHKV1B56fbvop4w6f2cTGEub9dRQ2Euta5VmqdX9', rewardManagerProgramAddress: 'testLsJKtyABc9UXJF8JWFKf1YH4LmqCWBC42c6akPb', rewardManagerStateAddress: 'DJPzVothq58SmkpRb1ATn5ddN2Rpv1j2TcGvM3XsHf1c', + paymentRouterProgramAddress: 'apaySbqV1XAmuiGszeN4NyWrXkkMrnuJVoNhzmS1AMa', rpcEndpoint: 'http://audius-protocol-solana-test-validator-1', usdcTokenMint: '26Q7gP8UfkDzi7GMFEQxTJaNJ8D2ybCUjex58M5MLu8y', wAudioTokenMint: '37RCjhgV1qGV2Q54EHFScdxZ22ydRMdKMtVgod47fDP3' diff --git a/packages/libs/src/sdk/sdk.ts b/packages/libs/src/sdk/sdk.ts index 2cbc652c464..e9b0fc49da2 100644 --- a/packages/libs/src/sdk/sdk.ts +++ b/packages/libs/src/sdk/sdk.ts @@ -9,11 +9,11 @@ import { DashboardWalletUsersApi } from './api/dashboard-wallet-users/DashboardW import { DeveloperAppsApi } from './api/developer-apps/DeveloperAppsApi' import { Configuration, TipsApi } from './api/generated/default' import { + TracksApi as TracksApiFull, Configuration as ConfigurationFull, PlaylistsApi as PlaylistsApiFull, ReactionsApi as ReactionsApiFull, SearchApi as SearchApiFull, - TracksApi as TracksApiFull, UsersApi as UsersApiFull, TipsApi as TipsApiFull, TransactionsApi as TransactionsApiFull @@ -30,6 +30,10 @@ import { addRequestSignatureMiddleware } from './middleware' import { OAuth } from './oauth' +import { + PaymentRouterClient, + getDefaultPaymentRouterClientConfig +} from './services' import { AntiAbuseOracle } from './services/AntiAbuseOracle/AntiAbuseOracle' import { getDefaultAntiAbuseOracleSelectorConfig } from './services/AntiAbuseOracleSelector' import { AntiAbuseOracleSelector } from './services/AntiAbuseOracleSelector/AntiAbuseOracleSelector' @@ -191,6 +195,13 @@ const initializeServices = (config: SdkConfig) => { solanaWalletAdapter }) + const paymentRouterClient = + config.services?.paymentRouterClient ?? + new PaymentRouterClient({ + ...getDefaultPaymentRouterClientConfig(servicesConfig), + solanaWalletAdapter + }) + const services: ServicesContainer = { storageNodeSelector, discoveryNodeSelector, @@ -200,6 +211,7 @@ const initializeServices = (config: SdkConfig) => { auth, claimableTokensClient, rewardManagerClient, + paymentRouterClient, solanaWalletAdapter, solanaRelay, antiAbuseOracle, @@ -231,7 +243,9 @@ const initializeApis = ({ services.storage, services.entityManager, services.auth, - services.logger + services.logger, + services.claimableTokensClient, + services.paymentRouterClient ) const users = new UsersApi( generatedApiClientConfig, diff --git a/packages/libs/src/sdk/services/DiscoveryNodeSelector/DiscoveryNodeSelector.test.ts b/packages/libs/src/sdk/services/DiscoveryNodeSelector/DiscoveryNodeSelector.test.ts index d038684c7d5..0689393000a 100644 --- a/packages/libs/src/sdk/services/DiscoveryNodeSelector/DiscoveryNodeSelector.test.ts +++ b/packages/libs/src/sdk/services/DiscoveryNodeSelector/DiscoveryNodeSelector.test.ts @@ -318,7 +318,7 @@ describe('discoveryNodeSelector', () => { const selector = new DiscoveryNodeSelector({ initialSelectedNode: BEHIND_BLOCKDIFF_NODE, blocklist: new Set([BEHIND_BLOCKDIFF_NODE]), - requestTimeout: 50, + requestTimeout: 1000, bootstrapServices: [ HEALTHY_NODE, UNHEALTHY_NODE, @@ -383,9 +383,10 @@ describe('discoveryNodeSelector', () => { }) test('removes backups when TTL is complete', async () => { + jest.useFakeTimers() const TEMP_BEHIND_BLOCKDIFF_NODE = 'https://temp-behind.audius.co' const selector = new DiscoveryNodeSelector({ - backupsTTL: 0, + backupsTTL: 10, unhealthyTTL: 0, bootstrapServices: [TEMP_BEHIND_BLOCKDIFF_NODE, HEALTHY_NODE].map( addDelegateOwnerWallets @@ -425,6 +426,9 @@ describe('discoveryNodeSelector', () => { } const middleware = selector.createMiddleware() + // Move time + jest.advanceTimersByTime(11) + // Trigger cleanup by retriggering selection await middleware.post!({ fetch, @@ -725,6 +729,17 @@ describe('discoveryNodeSelector', () => { }) ) + server.use( + rest.get(`${HEALTHY_NODE}/v1/full/tracks`, (_req, res, ctx) => { + return res( + ctx.status(200), + ctx.json({ + data: {} + }) + ) + }) + ) + const middleware = selector.createMiddleware() await middleware.onError!({ fetch, diff --git a/packages/libs/src/sdk/services/DiscoveryNodeSelector/healthChecks.ts b/packages/libs/src/sdk/services/DiscoveryNodeSelector/healthChecks.ts index 9573f2a3f60..e55eaff87f4 100644 --- a/packages/libs/src/sdk/services/DiscoveryNodeSelector/healthChecks.ts +++ b/packages/libs/src/sdk/services/DiscoveryNodeSelector/healthChecks.ts @@ -207,6 +207,31 @@ export const parseHealthStatusReason = ({ return { health: HealthCheckStatus.HEALTHY } } +const delay = async (ms: number, options?: { signal: AbortSignal }) => { + const signal = options?.signal + return new Promise((resolve, reject) => { + if (signal?.aborted) { + reject(new Error('aborted')) + } + const listener = () => { + clearTimeout(timer) + reject(new Error('aborted')) + } + const timer = setTimeout(() => { + signal?.removeEventListener('abort', listener) + resolve() + }, ms) + signal?.addEventListener('abort', listener) + }) +} + +const createTimeoutPromise = async (ms: number, signal: AbortSignal) => { + await delay(ms, { signal }) + if (!signal.aborted) { + throw new Error('timeout') + } +} + export const getDiscoveryNodeHealthCheck = async ({ endpoint, healthCheckThresholds, @@ -218,18 +243,20 @@ export const getDiscoveryNodeHealthCheck = async ({ fetchOptions?: RequestInit timeoutMs?: number }) => { + const ac = new AbortController() const timeoutPromises = [] if (timeoutMs !== undefined) { - const timeoutPromise = new Promise((_resolve, reject) => - setTimeout(() => reject(new Error('timeout')), timeoutMs) - ) - timeoutPromises.push(timeoutPromise) + timeoutPromises.push(createTimeoutPromise(timeoutMs, ac.signal)) } try { - const { data, comms } = await Promise.race([ - getHealthCheckData(endpoint, fetchOptions), + const res = await Promise.race([ + getHealthCheckData(endpoint, { ...fetchOptions, signal: ac.signal }), ...timeoutPromises ]) + if (!res) { + throw new Error('timeout') + } + const { data, comms } = res const reason = parseHealthStatusReason({ data, comms, @@ -242,5 +269,7 @@ export const getDiscoveryNodeHealthCheck = async ({ reason: (e as Error)?.message, data: null } + } finally { + ac.abort() } } diff --git a/packages/libs/src/sdk/services/Solana/constants.ts b/packages/libs/src/sdk/services/Solana/constants.ts new file mode 100644 index 00000000000..13ea54c6bbc --- /dev/null +++ b/packages/libs/src/sdk/services/Solana/constants.ts @@ -0,0 +1,9 @@ +import { PublicKey } from '@solana/web3.js' + +export const MEMO_PROGRAM_ID = new PublicKey( + 'Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo' +) + +export const MEMO_V2_PROGRAM_ID = new PublicKey( + 'MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr' +) diff --git a/packages/libs/src/sdk/services/Solana/index.ts b/packages/libs/src/sdk/services/Solana/index.ts index 289c869ed1c..4ddf3d9a3ee 100644 --- a/packages/libs/src/sdk/services/Solana/index.ts +++ b/packages/libs/src/sdk/services/Solana/index.ts @@ -2,4 +2,5 @@ export * from './SolanaRelay' export * from './SolanaRelayWalletAdapter' export * from './programs/ClaimableTokensClient' export * from './programs/RewardManagerClient' +export * from './programs/PaymentRouterClient' export * from './types' diff --git a/packages/libs/src/sdk/services/Solana/programs/BaseSolanaProgram.ts b/packages/libs/src/sdk/services/Solana/programs/BaseSolanaProgramClient.ts similarity index 96% rename from packages/libs/src/sdk/services/Solana/programs/BaseSolanaProgram.ts rename to packages/libs/src/sdk/services/Solana/programs/BaseSolanaProgramClient.ts index 2cdb1645dcf..91e9f33c3db 100644 --- a/packages/libs/src/sdk/services/Solana/programs/BaseSolanaProgram.ts +++ b/packages/libs/src/sdk/services/Solana/programs/BaseSolanaProgramClient.ts @@ -21,9 +21,9 @@ const isPublicKeyArray = (arr: any[]): arr is PublicKey[] => /** * Abstract class for initializing individual program clients. */ -export class BaseSolanaProgram { - /** The endpoint for the Solana RPC. */ - protected readonly connection: Connection +export class BaseSolanaProgramClient { + /** The Solana RPC client. */ + public readonly connection: Connection constructor( config: BaseSolanaProgramConfigInternal, protected wallet: SolanaWalletAdapter diff --git a/packages/libs/src/sdk/services/Solana/programs/ClaimableTokensClient/ClaimableTokensClient.ts b/packages/libs/src/sdk/services/Solana/programs/ClaimableTokensClient/ClaimableTokensClient.ts index 04e64b0a3de..f868943b1e7 100644 --- a/packages/libs/src/sdk/services/Solana/programs/ClaimableTokensClient/ClaimableTokensClient.ts +++ b/packages/libs/src/sdk/services/Solana/programs/ClaimableTokensClient/ClaimableTokensClient.ts @@ -1,4 +1,3 @@ -import { wAUDIO } from '@audius/fixed-decimal' import { ClaimableTokensProgram } from '@audius/spl' import { TransactionMessage, @@ -9,9 +8,10 @@ import { import { productionConfig } from '../../../../config/production' import { mergeConfigWithDefaults } from '../../../../utils/mergeConfigs' +import { mintFixedDecimalMap } from '../../../../utils/mintFixedDecimalMap' import { parseParams } from '../../../../utils/parseParams' import type { Mint } from '../../types' -import { BaseSolanaProgram } from '../BaseSolanaProgram' +import { BaseSolanaProgramClient } from '../BaseSolanaProgramClient' import { getDefaultClaimableTokensConfig } from './getDefaultConfig' import { @@ -32,7 +32,7 @@ import { * associated token accounts that are permissioned to users by their Ethereum * hedgehog wallet private keys. */ -export class ClaimableTokensClient extends BaseSolanaProgram { +export class ClaimableTokensClient extends BaseSolanaProgramClient { /** The program ID of the ClaimableTokensProgram instance. */ private readonly programId: PublicKey /** Map from token mint name to public key address. */ @@ -55,7 +55,7 @@ export class ClaimableTokensClient extends BaseSolanaProgram { }), USDC: ClaimableTokensProgram.deriveAuthority({ programId: configWithDefaults.programId, - mint: configWithDefaults.mints.wAUDIO + mint: configWithDefaults.mints.USDC }) } } @@ -167,7 +167,7 @@ export class ClaimableTokensClient extends BaseSolanaProgram { } const data = ClaimableTokensProgram.createSignedTransferInstructionData({ destination, - amount: wAUDIO(amount).value, + amount: mintFixedDecimalMap[mint](amount).value, nonce }) const [signature, recoveryId] = await auth.sign(data) @@ -190,7 +190,7 @@ export class ClaimableTokensClient extends BaseSolanaProgram { 'deriveUserBank', GetOrCreateUserBankSchema )(params) - return ClaimableTokensProgram.deriveUserBank({ + return await ClaimableTokensProgram.deriveUserBank({ ethAddress: ethWallet, claimableTokensPDA: this.authorities[mint] }) diff --git a/packages/libs/src/sdk/services/Solana/programs/PaymentRouterClient/PaymentRouterClient.ts b/packages/libs/src/sdk/services/Solana/programs/PaymentRouterClient/PaymentRouterClient.ts new file mode 100644 index 00000000000..5e790335ad8 --- /dev/null +++ b/packages/libs/src/sdk/services/Solana/programs/PaymentRouterClient/PaymentRouterClient.ts @@ -0,0 +1,260 @@ +import { PaymentRouterProgram } from '@audius/spl' +import { + Account, + TokenAccountNotFoundError, + TokenInvalidMintError, + TokenInvalidOwnerError, + createAssociatedTokenAccountIdempotentInstruction, + createTransferCheckedInstruction, + getAccount, + getAssociatedTokenAddressSync +} from '@solana/spl-token' +import { + PublicKey, + TransactionInstruction, + TransactionMessage, + VersionedTransaction +} from '@solana/web3.js' + +import { productionConfig } from '../../../../config/production' +import { mergeConfigWithDefaults } from '../../../../utils/mergeConfigs' +import { mintFixedDecimalMap } from '../../../../utils/mintFixedDecimalMap' +import { parseParams } from '../../../../utils/parseParams' +import { Prettify } from '../../../../utils/prettify' +import { MEMO_V2_PROGRAM_ID } from '../../constants' +import { Mint } from '../../types' +import { BaseSolanaProgramClient } from '../BaseSolanaProgramClient' + +import { getDefaultPaymentRouterClientConfig } from './getDefaultConfig' +import { + CreateMemoInstructionRequest, + CreateMemoInstructionSchema, + CreatePurchaseContentInstructionsRequest, + CreatePurchaseContentInstructionsSchema, + CreateRouteInstructionRequest, + CreateRouteInstructionSchema, + CreateTransferInstructionRequest, + CreateTransferInstructionSchema, + GetOrCreateProgramTokenAccountRequest, + GetOrCreateProgramTokenAccountSchema, + PaymentRouterClientConfig +} from './types' + +export class PaymentRouterClient extends BaseSolanaProgramClient { + private readonly programId: PublicKey + + /** The intermediate account where funds are sent to and routed from. */ + private readonly programAccount: PublicKey + private readonly programAccountBumpSeed: number + + private readonly mints: Prettify>> + + private existingTokenAccounts: Prettify>> + + constructor(config: PaymentRouterClientConfig) { + const configWithDefaults = mergeConfigWithDefaults( + config, + getDefaultPaymentRouterClientConfig(productionConfig) + ) + super(configWithDefaults, config.solanaWalletAdapter) + this.programId = configWithDefaults.programId + const [pda, bump] = PublicKey.findProgramAddressSync( + [new TextEncoder().encode('payment_router')], + this.programId + ) + this.programAccount = pda + this.programAccountBumpSeed = bump + this.mints = configWithDefaults.mints + this.existingTokenAccounts = {} + } + + public async createTransferInstruction( + params: CreateTransferInstructionRequest + ) { + const args = await parseParams( + 'crateTransferInstruction', + CreateTransferInstructionSchema + )(params) + const mint = this.mints[args.mint] + if (!mint) { + throw Error('Mint not configured') + } + const programTokenAccount = await this.getOrCreateProgramTokenAccount({ + mint: args.mint + }) + const sourceWallet = args.sourceWallet + const sourceTokenAccount = getAssociatedTokenAddressSync( + mint, + sourceWallet, + false + ) + const amount = mintFixedDecimalMap[args.mint](args.amount) + return createTransferCheckedInstruction( + sourceTokenAccount, + mint, + programTokenAccount.address, + sourceWallet, + amount.value, + amount.decimalPlaces + ) + } + + public async createRouteInstruction(params: CreateRouteInstructionRequest) { + const args = await parseParams( + 'createRouteInstruction', + CreateRouteInstructionSchema + )(params) + const programTokenAccount = await this.getOrCreateProgramTokenAccount({ + mint: args.mint + }) + const recipients: PublicKey[] = [] + const amounts: bigint[] = [] + for (const [key, value] of Object.entries(args.splits)) { + recipients.push(new PublicKey(key)) + amounts.push(value) + } + const totalAmount = mintFixedDecimalMap[args.mint](args.total).value + return PaymentRouterProgram.createRouteInstruction({ + sender: programTokenAccount.address, + senderOwner: this.programAccount, + paymentRouterPdaBump: this.programAccountBumpSeed, + recipients, + amounts, + totalAmount, + programId: this.programId + }) + } + + public async createPurchaseMemoInstruction( + params: CreateMemoInstructionRequest + ) { + const { + contentType, + contentId, + blockNumber, + buyerUserId, + accessType, + signer + } = await parseParams( + 'createMemoInstructionSchema', + CreateMemoInstructionSchema + )(params) + const memoString = `${contentType}:${contentId}:${blockNumber}:${buyerUserId}:${accessType}` + return new TransactionInstruction({ + keys: signer + ? [{ pubkey: signer, isSigner: true, isWritable: true }] + : [], + programId: MEMO_V2_PROGRAM_ID, + data: Buffer.from(memoString) + }) + } + + public async createPurchaseContentInstructions( + params: CreatePurchaseContentInstructionsRequest + ) { + const { + amount, + mint, + splits, + total, + contentId, + contentType, + blockNumber, + buyerUserId, + accessType, + sourceWallet + } = await parseParams( + 'createPurchaseContentInstructions', + CreatePurchaseContentInstructionsSchema + )(params) + return [ + this.createTransferInstruction({ + amount, + mint, + sourceWallet + }), + this.createRouteInstruction({ splits, total, mint }), + this.createPurchaseMemoInstruction({ + contentId, + contentType, + blockNumber, + buyerUserId, + accessType + }) + ] + } + + /** + * Creates or gets the intermediate funds token account for the program. + * Only needs to be created once per mint. + * @see {@link https://github.com/solana-labs/solana-program-library/blob/d72289c79a04411c69a8bf1054f7156b6196f9b3/token/js/src/actions/getOrCreateAssociatedTokenAccount.ts getOrCreateAssociatedTokenAccount} + */ + public async getOrCreateProgramTokenAccount( + params: GetOrCreateProgramTokenAccountRequest + ): Promise { + const args = await parseParams( + 'getOrCreateProgramTokenAccount', + GetOrCreateProgramTokenAccountSchema + )(params) + + // Check for cached account + const existingTokenAccount = this.existingTokenAccounts[args.mint] + if (existingTokenAccount) { + return existingTokenAccount + } + + const mint = this.mints[args.mint] + if (!mint) { + throw new Error(`Mint ${args.mint} not configured`) + } + const associatedTokenAdddress = getAssociatedTokenAddressSync( + mint, + this.programAccount, + true + ) + + let account: Account | null = null + try { + account = await getAccount(this.connection, associatedTokenAdddress) + this.existingTokenAccounts[args.mint] = account + } catch (error: unknown) { + if (error instanceof TokenAccountNotFoundError) { + // As this isn't atomic, it's possible others can create associated accounts meanwhile. + try { + const instruction = createAssociatedTokenAccountIdempotentInstruction( + await this.getFeePayer(), + associatedTokenAdddress, + this.programAccount, + mint + ) + const { lastValidBlockHeight, blockhash } = + await this.connection.getLatestBlockhash() + const msg = new TransactionMessage({ + payerKey: await this.getFeePayer(), + recentBlockhash: blockhash, + instructions: [instruction] + }) + const transaction = new VersionedTransaction(msg.compileToV0Message()) + const signature = await this.sendTransaction(transaction) + await this.connection.confirmTransaction( + { signature, blockhash, lastValidBlockHeight }, + 'finalized' + ) + } catch (e: unknown) { + // Ignore all errors; for now there is no API-compatible way to selectively ignore the expected + // instruction error if the associated account exists already. + } + + // Now this should always succeed + account = await getAccount(this.connection, associatedTokenAdddress) + } else { + throw error + } + } + + if (!account.mint.equals(mint)) throw new TokenInvalidMintError() + if (!account.owner.equals(this.programAccount)) + throw new TokenInvalidOwnerError() + return account + } +} diff --git a/packages/libs/src/sdk/services/Solana/programs/PaymentRouterClient/getDefaultConfig.ts b/packages/libs/src/sdk/services/Solana/programs/PaymentRouterClient/getDefaultConfig.ts new file mode 100644 index 00000000000..e106048e270 --- /dev/null +++ b/packages/libs/src/sdk/services/Solana/programs/PaymentRouterClient/getDefaultConfig.ts @@ -0,0 +1,16 @@ +import { PublicKey } from '@solana/web3.js' + +import { SdkServicesConfig } from '../../../../config/types' + +import { PaymentRouterClientConfigInternal } from './types' + +export const getDefaultPaymentRouterClientConfig = ( + config: SdkServicesConfig +): PaymentRouterClientConfigInternal => ({ + programId: new PublicKey(config.solana.paymentRouterProgramAddress), + rpcEndpoint: config.solana.rpcEndpoint, + mints: { + USDC: new PublicKey(config.solana.usdcTokenMint), + wAUDIO: new PublicKey(config.solana.wAudioTokenMint) + } +}) diff --git a/packages/libs/src/sdk/services/Solana/programs/PaymentRouterClient/index.ts b/packages/libs/src/sdk/services/Solana/programs/PaymentRouterClient/index.ts new file mode 100644 index 00000000000..3643f90bd15 --- /dev/null +++ b/packages/libs/src/sdk/services/Solana/programs/PaymentRouterClient/index.ts @@ -0,0 +1,3 @@ +export * from './PaymentRouterClient' +export * from './types' +export * from './getDefaultConfig' diff --git a/packages/libs/src/sdk/services/Solana/programs/PaymentRouterClient/types.ts b/packages/libs/src/sdk/services/Solana/programs/PaymentRouterClient/types.ts new file mode 100644 index 00000000000..bcdb63d7cfc --- /dev/null +++ b/packages/libs/src/sdk/services/Solana/programs/PaymentRouterClient/types.ts @@ -0,0 +1,72 @@ +import { PublicKey } from '@solana/web3.js' +import { z } from 'zod' + +import { HashId } from '../../../../types/HashId' +import { Prettify } from '../../../../utils/prettify' +import { + Mint, + MintSchema, + PublicKeySchema, + SolanaWalletAdapter +} from '../../types' +import { BaseSolanaProgramConfigInternal } from '../types' + +export type PaymentRouterClientConfigInternal = { + programId: PublicKey + mints: Prettify>> +} & BaseSolanaProgramConfigInternal + +export type PaymentRouterClientConfig = + Partial & { + solanaWalletAdapter: SolanaWalletAdapter + } + +export const CreateTransferInstructionSchema = z.object({ + mint: MintSchema, + amount: z.union([z.bigint(), z.number()]), + sourceWallet: PublicKeySchema +}) + +export type CreateTransferInstructionRequest = z.input< + typeof CreateTransferInstructionSchema +> + +export const CreateRouteInstructionSchema = z.object({ + mint: MintSchema, + splits: z.record(z.string(), z.bigint()), + total: z.union([z.bigint(), z.number()]) +}) + +export type CreateRouteInstructionRequest = z.input< + typeof CreateRouteInstructionSchema +> + +export const CreateMemoInstructionSchema = z.object({ + contentType: z.enum(['track', 'album']), + contentId: HashId.or(z.number()), + blockNumber: z.number(), + buyerUserId: HashId.or(z.number()), + accessType: z.enum(['stream', 'download']), + signer: PublicKeySchema.optional() +}) + +export type CreateMemoInstructionRequest = z.input< + typeof CreateMemoInstructionSchema +> + +export const CreatePurchaseContentInstructionsSchema = + CreateTransferInstructionSchema.extend( + CreateRouteInstructionSchema.shape + ).extend(CreateMemoInstructionSchema.shape) + +export type CreatePurchaseContentInstructionsRequest = z.input< + typeof CreatePurchaseContentInstructionsSchema +> + +export const GetOrCreateProgramTokenAccountSchema = z.object({ + mint: MintSchema +}) + +export type GetOrCreateProgramTokenAccountRequest = z.input< + typeof GetOrCreateProgramTokenAccountSchema +> diff --git a/packages/libs/src/sdk/services/Solana/programs/RewardManagerClient/RewardManagerClient.ts b/packages/libs/src/sdk/services/Solana/programs/RewardManagerClient/RewardManagerClient.ts index bc9d0c0721a..1edae9f976a 100644 --- a/packages/libs/src/sdk/services/Solana/programs/RewardManagerClient/RewardManagerClient.ts +++ b/packages/libs/src/sdk/services/Solana/programs/RewardManagerClient/RewardManagerClient.ts @@ -5,7 +5,7 @@ import { Secp256k1Program, type PublicKey } from '@solana/web3.js' import { productionConfig } from '../../../../config/production' import { mergeConfigWithDefaults } from '../../../../utils/mergeConfigs' import { parseParams } from '../../../../utils/parseParams' -import { BaseSolanaProgram } from '../BaseSolanaProgram' +import { BaseSolanaProgramClient } from '../BaseSolanaProgramClient' import { getDefaultRewardManagerClentConfig } from './getDefaultConfig' import { @@ -29,7 +29,7 @@ import { * based on attestations from N uniquely owned discovery nodes and an anti abuse * oracle node. */ -export class RewardManagerClient extends BaseSolanaProgram { +export class RewardManagerClient extends BaseSolanaProgramClient { private readonly programId: PublicKey private readonly rewardManagerStateAccount: PublicKey private readonly authority: PublicKey diff --git a/packages/libs/src/sdk/types.ts b/packages/libs/src/sdk/types.ts index 27166ad9a79..8a774cf5930 100644 --- a/packages/libs/src/sdk/types.ts +++ b/packages/libs/src/sdk/types.ts @@ -6,7 +6,11 @@ import type { AuthService } from './services/Auth' import type { DiscoveryNodeSelectorService } from './services/DiscoveryNodeSelector' import type { EntityManagerService } from './services/EntityManager' import type { LoggerService } from './services/Logger' -import type { SolanaRelayService, SolanaWalletAdapter } from './services/Solana' +import type { + PaymentRouterClient, + SolanaRelayService, + SolanaWalletAdapter +} from './services/Solana' import { ClaimableTokensClient } from './services/Solana/programs/ClaimableTokensClient' import { RewardManagerClient } from './services/Solana/programs/RewardManagerClient' import type { StorageService } from './services/Storage' @@ -58,6 +62,11 @@ export type ServicesContainer = { */ claimableTokensClient: ClaimableTokensClient + /** + * Payment Router Program client for Solana + */ + paymentRouterClient: PaymentRouterClient + /** * Reward Manager Program client for Solana */ diff --git a/packages/libs/src/sdk/utils/mintFixedDecimalMap.ts b/packages/libs/src/sdk/utils/mintFixedDecimalMap.ts new file mode 100644 index 00000000000..19537c0c38d --- /dev/null +++ b/packages/libs/src/sdk/utils/mintFixedDecimalMap.ts @@ -0,0 +1,6 @@ +import { USDC, wAUDIO } from '@audius/fixed-decimal' + +export const mintFixedDecimalMap = { + USDC, + wAUDIO +} diff --git a/packages/libs/src/services/solana/SolanaWeb3Manager.ts b/packages/libs/src/services/solana/SolanaWeb3Manager.ts index 8b179f41bc9..686e350903d 100644 --- a/packages/libs/src/services/solana/SolanaWeb3Manager.ts +++ b/packages/libs/src/services/solana/SolanaWeb3Manager.ts @@ -1,4 +1,4 @@ -import { route } from '@audius/spl' +import { PaymentRouterProgram } from '@audius/spl' import * as splToken from '@solana/spl-token' import { TOKEN_PROGRAM_ID, @@ -707,16 +707,19 @@ export class SolanaWeb3Manager { USDC_DECIMALS ) - const paymentRouterInstruction = await route( - paymentRouterTokenAccount, - paymentRouterPda, - paymentRouterPdaBump, - Object.keys(recipientAmounts).map((key) => new PublicKey(key)), // recipients - amounts, - totalAmount, - TOKEN_PROGRAM_ID, - this.paymentRouterProgramId - ) + const paymentRouterInstruction = + await PaymentRouterProgram.createRouteInstruction({ + sender: paymentRouterTokenAccount, + senderOwner: paymentRouterPda, + paymentRouterPdaBump, + recipients: Object.keys(recipientAmounts).map( + (key) => new PublicKey(key) + ), + amounts, + totalAmount, + tokenProgramId: TOKEN_PROGRAM_ID, + programId: this.paymentRouterProgramId + }) const data = `${type}:${id}:${blocknumber}:${purchaserUserId}:${purchaseAccess}` @@ -755,7 +758,7 @@ export class SolanaWeb3Manager { purchaseAccess }: { id: number - type: 'track' + type: PurchaseContentType splits: Record extraAmount?: number | BN blocknumber: number diff --git a/packages/mobile/src/app/App.tsx b/packages/mobile/src/app/App.tsx index 01a51b7812b..32cec5b5fb2 100644 --- a/packages/mobile/src/app/App.tsx +++ b/packages/mobile/src/app/App.tsx @@ -6,7 +6,7 @@ import { SafeAreaProvider, initialWindowMetrics } from 'react-native-safe-area-context' -import TrackPlayer from 'react-native-track-player' +import TrackPlayer, { IOSCategory } from 'react-native-track-player' import { Provider } from 'react-redux' import { useEffectOnce } from 'react-use' import { PersistGate } from 'redux-persist/integration/react' @@ -64,7 +64,13 @@ const App = () => { useEffectOnce(() => { setLibs(null) subscribeToNetworkStatusUpdates() - TrackPlayer.setupPlayer({ autoHandleInterruptions: true }) + TrackPlayer.setupPlayer({ + minBuffer: 0.1, + playBuffer: 0.1, + waitForBuffer: false, + autoHandleInterruptions: true, + iosCategory: IOSCategory.Playback + }) }) useEnterForeground(() => { diff --git a/packages/mobile/src/components/audio/AudioPlayer.tsx b/packages/mobile/src/components/audio/AudioPlayer.tsx index 8ae817875aa..7ce9e9a5a52 100644 --- a/packages/mobile/src/components/audio/AudioPlayer.tsx +++ b/packages/mobile/src/components/audio/AudioPlayer.tsx @@ -37,10 +37,10 @@ import TrackPlayer, { Capability, Event, State, - usePlaybackState, useTrackPlayerEvents, RepeatMode as TrackPlayerRepeatMode, - TrackType + TrackType, + PitchAlgorithm } from 'react-native-track-player' import { useDispatch, useSelector } from 'react-redux' import { useAsync, usePrevious } from 'react-use' @@ -166,7 +166,6 @@ export const AudioPlayer = () => { FeatureFlags.PODCAST_CONTROL_UPDATES_ENABLED, FeatureFlags.PODCAST_CONTROL_UPDATES_ENABLED_FALLBACK ) - const playbackState = usePlaybackState() const track = useSelector(getCurrentTrack) const playing = useSelector(getPlaying) const seek = useSelector(getSeek) @@ -609,6 +608,8 @@ export const AudioPlayer = () => { return { url, type: TrackType.Default, + contentType: 'audio/mpeg', + pitchAlgorithm: PitchAlgorithm.Music, title: track.title, artist: trackOwner.name, genre: track.genre, @@ -652,13 +653,12 @@ export const AudioPlayer = () => { } else { await TrackPlayer.reset() + await TrackPlayer.play() + const firstTrack = newQueueTracks[queueIndex] if (!firstTrack) return - await TrackPlayer.add(await makeTrackData(firstTrack)) - if (playing) { - await TrackPlayer.play() - } + await TrackPlayer.add(await makeTrackData(firstTrack)) enqueueTracksJobRef.current = enqueueTracks(newQueueTracks, queueIndex) await enqueueTracksJobRef.current @@ -675,8 +675,7 @@ export const AudioPlayer = () => { isCollectionMarkedForDownload, isNotReachable, storageNodeSelector, - nftAccessSignatureMap, - playing + nftAccessSignatureMap ]) const handleQueueIdxChange = useCallback(async () => { @@ -694,17 +693,12 @@ export const AudioPlayer = () => { }, [queueIndex]) const handleTogglePlay = useCallback(async () => { - if (playbackState.state === State.Playing && !playing) { - await TrackPlayer.pause() - } else if ( - (playbackState.state === State.Paused || - playbackState.state === State.Ready || - playbackState.state === State.Stopped) && - playing - ) { + if (playing) { await TrackPlayer.play() + } else { + await TrackPlayer.pause() } - }, [playbackState, playing]) + }, [playing]) const handleStop = useCallback(async () => { TrackPlayer.reset() diff --git a/packages/mobile/src/components/card/CollectionDogEar.tsx b/packages/mobile/src/components/card/CollectionDogEar.tsx index ec8b02aab11..857ba81c226 100644 --- a/packages/mobile/src/components/card/CollectionDogEar.tsx +++ b/packages/mobile/src/components/card/CollectionDogEar.tsx @@ -3,7 +3,7 @@ import { DogEarType } from '@audius/common/models' import { cacheCollectionsSelectors } from '@audius/common/store' import { useSelector } from 'react-redux' -import { DogEar } from '../core' +import { DogEar } from 'app/components/core' const { getCollection } = cacheCollectionsSelectors diff --git a/packages/mobile/src/components/challenge-rewards-drawer/ClaimAllRewardsDrawer.tsx b/packages/mobile/src/components/challenge-rewards-drawer/ClaimAllRewardsDrawer.tsx index 908045fcfe8..50c7d4d9549 100644 --- a/packages/mobile/src/components/challenge-rewards-drawer/ClaimAllRewardsDrawer.tsx +++ b/packages/mobile/src/components/challenge-rewards-drawer/ClaimAllRewardsDrawer.tsx @@ -27,8 +27,8 @@ const { getClaimStatus } = audioRewardsPageSelectors const messages = { // Claim success toast claimSuccessMessage: 'All rewards successfully claimed!', - pending: (amount) => `${amount} Pending`, - claimAudio: (amount) => `Claim ${amount} $AUDIO`, + pending: (amount: number) => `${amount} Pending`, + claimAudio: (amount: number) => `Claim ${amount} $AUDIO`, done: 'Done' } @@ -60,17 +60,18 @@ export const ClaimAllRewardsDrawer = () => { const { toast } = useToast() const claimStatus = useSelector(getClaimStatus) const { onClose } = useDrawerState(MODAL_NAME) - const { claimableChallenges, cooldownChallenges, summary } = + const { claimableAmount, claimableChallenges, cooldownChallenges, summary } = useChallengeCooldownSchedule({ multiple: true }) const claimInProgress = claimStatus === ClaimStatus.CUMULATIVE_CLAIMING + const hasClaimed = claimStatus === ClaimStatus.CUMULATIVE_SUCCESS useEffect(() => { - if (claimStatus === ClaimStatus.CUMULATIVE_SUCCESS) { + if (hasClaimed) { toast({ content: messages.claimSuccessMessage, type: 'info' }) } - }, [claimStatus, toast]) + }, [hasClaimed, toast]) const handleClose = useCallback(() => { dispatch(resetAndCancelClaimReward()) @@ -114,15 +115,16 @@ export const ClaimAllRewardsDrawer = () => { - {summary && summary?.value > 0 ? ( + {claimableAmount > 0 && !hasClaimed ? ( ) : ( @@ -98,6 +99,7 @@ export const CollectionActionButtons = (props: CollectionActionButtonProps) => { iconLeft={isPlaying && isPreviewing ? IconPause : IconPlay} onClick={onPreview} widthToHideText={BUTTON_COLLAPSE_WIDTHS.first} + size='large' > {isPlaying && isPreviewing ? messages.pause : messages.preview} diff --git a/packages/web/src/components/collection/desktop/CollectionHeader.tsx b/packages/web/src/components/collection/desktop/CollectionHeader.tsx index dfc626188eb..702e6660318 100644 --- a/packages/web/src/components/collection/desktop/CollectionHeader.tsx +++ b/packages/web/src/components/collection/desktop/CollectionHeader.tsx @@ -220,7 +220,7 @@ export const CollectionHeader = (props: CollectionHeaderProps) => { css={{ background: 0, border: 0, padding: 0, margin: 0 }} gap='s' alignItems='center' - className={cn(styles.title, { + className={cn({ [styles.editableTitle]: isOwner })} onClick={isOwner ? handleClickEditTitle : undefined} @@ -229,6 +229,7 @@ export const CollectionHeader = (props: CollectionHeaderProps) => { variant='heading' size='xl' className={cn(styles.titleHeader, fadeIn)} + textAlign='left' > {title} diff --git a/packages/web/src/components/collection/desktop/OverflowMenuButton.tsx b/packages/web/src/components/collection/desktop/OverflowMenuButton.tsx index 7774df3e639..405eae5def4 100644 --- a/packages/web/src/components/collection/desktop/OverflowMenuButton.tsx +++ b/packages/web/src/components/collection/desktop/OverflowMenuButton.tsx @@ -33,6 +33,7 @@ export const OverflowMenuButton = (props: OverflowMenuButtonProps) => { is_album, playlist_name, is_private, + is_stream_gated, playlist_owner_id, has_current_user_saved, permalink, @@ -71,7 +72,7 @@ export const OverflowMenuButton = (props: OverflowMenuButtonProps) => { isFavorited: has_current_user_saved, mount: 'page', isOwner, - includeEmbed: true, + includeEmbed: !is_private && !is_stream_gated, includeFavorite: hasStreamAccess, includeVisitPage: false, isPublic: !is_private, diff --git a/packages/web/src/components/collection/mobile/CollectionHeader.module.css b/packages/web/src/components/collection/mobile/CollectionHeader.module.css index 1266222d64f..fe8b2e63283 100644 --- a/packages/web/src/components/collection/mobile/CollectionHeader.module.css +++ b/packages/web/src/components/collection/mobile/CollectionHeader.module.css @@ -11,6 +11,12 @@ background-repeat: no-repeat; } +.borderOffset { + position: absolute; + top: calc(-1 * var(--border-width)); + left: calc(-1 * var(--border-width)); +} + /* Loading */ .loadingSkeleton { diff --git a/packages/web/src/components/collection/mobile/CollectionHeader.tsx b/packages/web/src/components/collection/mobile/CollectionHeader.tsx index 7988347890b..dd9ad7b8e25 100644 --- a/packages/web/src/components/collection/mobile/CollectionHeader.tsx +++ b/packages/web/src/components/collection/mobile/CollectionHeader.tsx @@ -10,9 +10,11 @@ import { PurchaseableContentType, useEditPlaylistModal } from '@audius/common/store' +import { getDogEarType } from '@audius/common/utils' import { Box, Button, Flex, IconPause, IconPlay, Text } from '@audius/harmony' import cn from 'classnames' +import { DogEar } from 'components/dog-ear' import DynamicImage from 'components/dynamic-image/DynamicImage' import { UserLink } from 'components/link' import Skeleton from 'components/skeleton/Skeleton' @@ -112,6 +114,7 @@ const CollectionHeader = ({ ) const { hasStreamAccess } = useGatedContentAccess(collection) const isPremium = collection?.is_stream_gated + const isUnlisted = collection?.is_private // If user doesn't have access, show preview only. If user has access, show play only. // If user is owner, show both. @@ -185,8 +188,26 @@ const CollectionHeader = ({ ) } + const renderDogEar = () => { + const DogEarType = getDogEarType({ + isUnlisted, + streamConditions, + isOwner, + hasStreamAccess + }) + if (!isLoading && DogEarType) { + return ( +
+ +
+ ) + } + return null + } + return ( + {renderDogEar()} {type === 'playlist' && !isPublished diff --git a/packages/web/src/components/collections-table/CollectionsTableOverflowMenuButton.tsx b/packages/web/src/components/collections-table/CollectionsTableOverflowMenuButton.tsx index ba05ecbe8a7..fbe079e01b2 100644 --- a/packages/web/src/components/collections-table/CollectionsTableOverflowMenuButton.tsx +++ b/packages/web/src/components/collections-table/CollectionsTableOverflowMenuButton.tsx @@ -22,6 +22,7 @@ export const CollectionsTableOverflowMenuButton = ( is_album: isAlbum, playlist_owner_id: playlistOwnerId, is_private: isPrivate, + is_stream_gated: isStreamGated, permalink } = (useSelector((state: CommonState) => getCollection(state, { id: collectionId }) @@ -30,7 +31,7 @@ export const CollectionsTableOverflowMenuButton = ( const overflowMenu = { type: isAlbum ? 'album' : 'playlist', playlistId: collectionId, - includeEmbed: true, + includeEmbed: !isPrivate && !isStreamGated, includeVisitArtistPage: false, includeShare: true, includeEdit: true, diff --git a/packages/web/src/components/create-playlist/PlaylistForm.tsx b/packages/web/src/components/create-playlist/PlaylistForm.tsx index 3a0130149f8..2c240acc745 100644 --- a/packages/web/src/components/create-playlist/PlaylistForm.tsx +++ b/packages/web/src/components/create-playlist/PlaylistForm.tsx @@ -110,12 +110,10 @@ const PlaylistForm = ({ {isAlbum ? ( - - - + ) : null} ( const preview = previewOverride ? ( previewOverride(toggleMenu) ) : ( - +
diff --git a/packages/web/src/components/link/UserLink.tsx b/packages/web/src/components/link/UserLink.tsx index a72cfea419a..33b474830ff 100644 --- a/packages/web/src/components/link/UserLink.tsx +++ b/packages/web/src/components/link/UserLink.tsx @@ -1,6 +1,6 @@ import { ID } from '@audius/common/models' import { cacheUsersSelectors } from '@audius/common/store' -import { IconSize, Text, TextVariant, useTheme } from '@audius/harmony' +import { IconSize, Text, useTheme } from '@audius/harmony' import { ArtistPopover } from 'components/artist/ArtistPopover' import UserBadges from 'components/user-badges/UserBadges' @@ -16,7 +16,6 @@ type UserLinkProps = Omit & { userId: ID badgeSize?: IconSize popover?: boolean - textVariant?: TextVariant } export const UserLink = (props: UserLinkProps) => { diff --git a/packages/web/src/components/premium-content-purchase-modal/PremiumContentPurchaseModal.tsx b/packages/web/src/components/premium-content-purchase-modal/PremiumContentPurchaseModal.tsx index 1e22107ec9a..114be15800f 100644 --- a/packages/web/src/components/premium-content-purchase-modal/PremiumContentPurchaseModal.tsx +++ b/packages/web/src/components/premium-content-purchase-modal/PremiumContentPurchaseModal.tsx @@ -105,6 +105,10 @@ const RenderForm = ({ usePurchaseContentFormState({ price }) const [, , { setValue: setPurchaseMethod }] = useField(PURCHASE_METHOD) const currentPageIndex = pageToPageIndex(page) + const isLinkDisabled = + stage === PurchaseContentStage.START || + stage === PurchaseContentStage.PURCHASING || + stage === PurchaseContentStage.CONFIRMING_PURCHASE const { submitForm, resetForm } = useFormikContext() const { history } = useHistoryContext() @@ -156,6 +160,7 @@ const RenderForm = ({ showLabel={false} metadata={metadata} owner={metadata.user} + disabled={isLinkDisabled} /> diff --git a/packages/web/src/components/track/LockedContentDetailsTile.tsx b/packages/web/src/components/track/LockedContentDetailsTile.tsx index 74336ac98a8..933ee48b44e 100644 --- a/packages/web/src/components/track/LockedContentDetailsTile.tsx +++ b/packages/web/src/components/track/LockedContentDetailsTile.tsx @@ -39,12 +39,14 @@ export type LockedContentDetailsTileProps = { metadata: PurchaseableContentMetadata | Track | Collection owner: UserMetadata showLabel?: boolean + disabled?: boolean } export const LockedContentDetailsTile = ({ metadata, owner, - showLabel = true + showLabel = true, + disabled = false }: LockedContentDetailsTileProps) => { const { stream_conditions: streamConditions } = metadata const isAlbum = 'playlist_id' in metadata @@ -146,6 +148,7 @@ export const LockedContentDetailsTile = ({ textVariant='title' strength='weak' userId={owner.user_id} + disabled={disabled} /> diff --git a/packages/web/src/components/track/desktop/ConnectedPlaylistTile.tsx b/packages/web/src/components/track/desktop/ConnectedPlaylistTile.tsx index be4c6cfe99b..422c1986642 100644 --- a/packages/web/src/components/track/desktop/ConnectedPlaylistTile.tsx +++ b/packages/web/src/components/track/desktop/ConnectedPlaylistTile.tsx @@ -293,7 +293,7 @@ const ConnectedPlaylistTile = ({ playlistName: title, isPublic: !isUnlisted, isOwner, - includeEmbed: true, + includeEmbed: !isUnlisted && !isStreamGated, includeShare: false, includeRepost: false, includeFavorite: false, diff --git a/packages/web/src/components/track/mobile/TrackListItem.tsx b/packages/web/src/components/track/mobile/TrackListItem.tsx index 3d34296731d..6113cd99fa6 100644 --- a/packages/web/src/components/track/mobile/TrackListItem.tsx +++ b/packages/web/src/components/track/mobile/TrackListItem.tsx @@ -173,7 +173,8 @@ const TrackListItem = ({ const isUsdcPurchaseGated = isContentUSDCPurchaseGated(streamConditions) const onClickTrack = () => { - if (uid && !isLocked && !isDeleted && togglePlay) togglePlay(uid, trackId) + if (uid && !isDeleted && (!isLocked || isUsdcPurchaseGated) && togglePlay) + togglePlay(uid, trackId) } const onRemoveTrack = (e: MouseEvent) => { diff --git a/packages/web/src/components/tracks-table/TracksTable.tsx b/packages/web/src/components/tracks-table/TracksTable.tsx index b5ca6095e73..4b15b65c3ab 100644 --- a/packages/web/src/components/tracks-table/TracksTable.tsx +++ b/packages/web/src/components/tracks-table/TracksTable.tsx @@ -200,10 +200,6 @@ export const TracksTable = ({ const renderTrackNameCell = useCallback( (cellInfo: TrackCell) => { const track = cellInfo.row.original - const { isFetchingNFTAccess, hasStreamAccess } = trackAccessMap[ - track.track_id - ] ?? { isFetchingNFTAccess: false, hasStreamAccess: true } - const isLocked = !isFetchingNFTAccess && !hasStreamAccess const index = cellInfo.row.index const active = index === playingIndex const deleted = @@ -212,7 +208,7 @@ export const TracksTable = ({ return (
) }, - [trackAccessMap, playingIndex] + [playingIndex] ) const renderArtistNameCell = useCallback( diff --git a/packages/web/src/pages/audio-rewards-page/ChallengeRewardsTile.tsx b/packages/web/src/pages/audio-rewards-page/ChallengeRewardsTile.tsx index 3e91e9bfd3c..5a9731bf709 100644 --- a/packages/web/src/pages/audio-rewards-page/ChallengeRewardsTile.tsx +++ b/packages/web/src/pages/audio-rewards-page/ChallengeRewardsTile.tsx @@ -254,7 +254,7 @@ const RewardPanel = ({ } const ClaimAllPanel = () => { - const isMobile = useIsMobile() + const isMobile = useIsMobile() || window.innerWidth < 1080 const wm = useWithMobileStyle(styles.mobile) const { cooldownChallenges, cooldownAmount, claimableAmount, isEmpty } = useChallengeCooldownSchedule({ multiple: true }) @@ -266,6 +266,13 @@ const ClaimAllPanel = () => { const onClickMoreInfo = useCallback(() => { setClaimAllRewardsVisibility(true) }, [setClaimAllRewardsVisibility]) + const handleClick = useCallback(() => { + if (claimableAmount > 0) { + onClickClaimAllRewards() + } else if (cooldownAmount > 0) { + onClickMoreInfo() + } + }, [claimableAmount, cooldownAmount, onClickClaimAllRewards, onClickMoreInfo]) if (isMobile) { return ( @@ -277,6 +284,8 @@ const ClaimAllPanel = () => { alignSelf='stretch' justifyContent='space-between' m='s' + css={{ cursor: 'pointer' }} + onClick={handleClick} > @@ -347,13 +356,17 @@ const ClaimAllPanel = () => { alignSelf='stretch' justifyContent='space-between' m='s' + css={{ cursor: 'pointer' }} + onClick={handleClick} > - + {claimableAmount > 0 ? ( + + ) : null} {isEmpty ? null : ( diff --git a/packages/web/src/pages/audio-rewards-page/components/modals/ChallengeRewardsModal/ClaimAllRewardsModal.tsx b/packages/web/src/pages/audio-rewards-page/components/modals/ChallengeRewardsModal/ClaimAllRewardsModal.tsx index 4c9ee8e4340..d753ff0578f 100644 --- a/packages/web/src/pages/audio-rewards-page/components/modals/ChallengeRewardsModal/ClaimAllRewardsModal.tsx +++ b/packages/web/src/pages/audio-rewards-page/components/modals/ChallengeRewardsModal/ClaimAllRewardsModal.tsx @@ -42,7 +42,8 @@ const messages = { } const { show: showConfetti } = musicConfettiActions -const { claimAllChallengeRewards } = audioRewardsPageActions +const { claimAllChallengeRewards, resetAndCancelClaimReward } = + audioRewardsPageActions const { getClaimStatus } = audioRewardsPageSelectors export const ClaimAllRewardsModal = () => { @@ -50,20 +51,20 @@ export const ClaimAllRewardsModal = () => { const { toast } = useContext(ToastContext) const wm = useWithMobileStyle(styles.mobile) const [isOpen, setOpen] = useModalState('ClaimAllRewards') - const [isHCaptchaModalOpen] = useModalState('HCaptcha') const claimStatus = useSelector(getClaimStatus) const { claimableAmount, claimableChallenges, cooldownChallenges, summary } = useChallengeCooldownSchedule({ multiple: true }) const claimInProgress = claimStatus === ClaimStatus.CUMULATIVE_CLAIMING + const hasClaimed = claimStatus === ClaimStatus.CUMULATIVE_SUCCESS useEffect(() => { - if (claimStatus === ClaimStatus.CUMULATIVE_SUCCESS) { + if (hasClaimed) { toast(messages.rewardsClaimed, CLAIM_REWARD_TOAST_TIMEOUT_MILLIS) dispatch(showConfetti()) } - }, [claimStatus, toast, dispatch]) + }, [toast, dispatch, hasClaimed]) const onClaimRewardClicked = useCallback(() => { const claims = claimableChallenges.map((challenge) => ({ @@ -76,6 +77,11 @@ export const ClaimAllRewardsModal = () => { dispatch(claimAllChallengeRewards({ claims })) }, [dispatch, claimableChallenges]) + const handleClose = useCallback(() => { + dispatch(resetAndCancelClaimReward()) + setOpen(false) + }, [dispatch, setOpen]) + const formatLabel = useCallback((item: any) => { const { label, claimableDate, isClose } = item const formattedLabel = isClose ? ( @@ -97,13 +103,11 @@ export const ClaimAllRewardsModal = () => { title={messages.rewards} showTitleHeader isOpen={isOpen} - onClose={() => setOpen(false)} + onClose={handleClose} isFullscreen={true} useGradientTitle={false} titleClassName={wm(styles.title)} headerContainerClassName={styles.header} - showDismissButton={!isHCaptchaModalOpen} - dismissOnClickOutside={!isHCaptchaModalOpen} > @@ -120,8 +124,9 @@ export const ClaimAllRewardsModal = () => { summaryLabelColor='accent' summaryValueColor='default' /> - {claimableAmount > 0 ? ( + {claimableAmount > 0 && !hasClaimed ? (
{isAlbum && showPremiumAlbums ? ( - + + + ) : null}
{messages.trackDetails.title}