From 328e805f46acf4e022c4df5957d70a483c1c59b2 Mon Sep 17 00:00:00 2001 From: Reed <3893871+dharit-tan@users.noreply.github.com> Date: Thu, 8 Feb 2024 12:40:23 -0500 Subject: [PATCH] [PAY-2450][PAY-2451] Purchase indexing uses access from memo (#7510) --- packages/common/src/models/PurchaseContent.ts | 5 + .../src/services/audius-backend/solana.ts | 22 +- .../src/store/purchase-content/sagas.ts | 41 +- .../tasks/payment_router_mock_transactions.py | 593 ++++++++++++++++++ .../tasks/test_index_payment_router.py | 70 ++- .../tasks/test_index_user_bank.py | 77 ++- .../tasks/user_bank_mock_transactions.py | 353 ++++++++++- .../src/tasks/index_payment_router.py | 136 ++-- .../src/tasks/index_user_bank.py | 18 +- .../src/services/solana/SolanaWeb3Manager.ts | 23 +- 10 files changed, 1245 insertions(+), 93 deletions(-) diff --git a/packages/common/src/models/PurchaseContent.ts b/packages/common/src/models/PurchaseContent.ts index 28180a5f16a..3a1dd33d003 100644 --- a/packages/common/src/models/PurchaseContent.ts +++ b/packages/common/src/models/PurchaseContent.ts @@ -8,3 +8,8 @@ export enum PurchaseVendor { STRIPE = 'Stripe', COINFLOW = 'Coinflow' } + +export enum PurchaseAccess { + STREAM = 'stream', + DOWNLOAD = 'download' +} diff --git a/packages/common/src/services/audius-backend/solana.ts b/packages/common/src/services/audius-backend/solana.ts index 5dcf7c30b94..3bab93d3a01 100644 --- a/packages/common/src/services/audius-backend/solana.ts +++ b/packages/common/src/services/audius-backend/solana.ts @@ -20,7 +20,13 @@ import BN from 'bn.js' import { BN_USDC_CENT_WEI } from '~/utils/wallet' -import { AnalyticsEvent, ID, Name, SolanaWalletAddress } from '../../models' +import { + AnalyticsEvent, + ID, + Name, + SolanaWalletAddress, + PurchaseAccess +} from '../../models' import { AudiusBackend } from './AudiusBackend' @@ -378,6 +384,7 @@ export type PurchaseContentArgs = { type: 'track' splits: Record purchaserUserId: ID + purchaseAccess: PurchaseAccess } export const purchaseContent = async ( audiusBackendInstance: AudiusBackend, @@ -397,6 +404,7 @@ export type PurchaseContentWithPaymentRouterArgs = { recentBlockhash?: string purchaserUserId: ID wallet: Keypair + purchaseAccess: PurchaseAccess } export const purchaseContentWithPaymentRouter = async ( @@ -408,7 +416,8 @@ export const purchaseContentWithPaymentRouter = async ( extraAmount = 0, purchaserUserId, splits, - wallet + wallet, + purchaseAccess }: PurchaseContentWithPaymentRouterArgs ) => { const solanaWeb3Manager = (await audiusBackendInstance.getAudiusLibs()) @@ -421,7 +430,8 @@ export const purchaseContentWithPaymentRouter = async ( splits, purchaserUserId, senderKeypair: wallet, - skipSendAndReturnTransaction: true + skipSendAndReturnTransaction: true, + purchaseAccess }) return tx } @@ -474,7 +484,8 @@ export const createRootWalletRecoveryTransaction = async ( blocknumber: 0, // ignored splits: { [userBank.toString()]: new BN(amount.toString()) }, purchaserUserId: 0, // ignored - senderAccount: wallet.publicKey + senderAccount: wallet.publicKey, + purchaseAccess: PurchaseAccess.STREAM //ignored }) const recentBlockhash = await getRecentBlockhash(audiusBackendInstance) @@ -660,7 +671,8 @@ export const createPaymentRouterRouteTransaction = async ( blocknumber: 0, // ignored splits, purchaserUserId: 0, // ignored - senderAccount: sender + senderAccount: sender, + purchaseAccess: PurchaseAccess.STREAM //ignored }) return new Transaction({ recentBlockhash: blockhash, diff --git a/packages/common/src/store/purchase-content/sagas.ts b/packages/common/src/store/purchase-content/sagas.ts index 2101d66a8f7..21b9f9e8fc8 100644 --- a/packages/common/src/store/purchase-content/sagas.ts +++ b/packages/common/src/store/purchase-content/sagas.ts @@ -5,7 +5,11 @@ import { call, put, race, select, take } from 'typed-redux-saga' import { FavoriteSource, Name } from '~/models/Analytics' import { ErrorLevel } from '~/models/ErrorReporting' import { ID } from '~/models/Identifiers' -import { PurchaseMethod, PurchaseVendor } from '~/models/PurchaseContent' +import { + PurchaseMethod, + PurchaseVendor, + PurchaseAccess +} from '~/models/PurchaseContent' import { Track, isContentUSDCPurchaseGated } from '~/models/Track' import { User } from '~/models/User' import { BNUSDC } from '~/models/Wallet' @@ -94,6 +98,12 @@ function* getContentInfo({ contentId, contentType }: GetPurchaseConfigArgs) { const trackInfo = yield* select(getTrack, { id: contentId }) const purchaseConditions = trackInfo?.stream_conditions ?? trackInfo?.download_conditions + // Stream access is a superset of download access - purchasing a stream-gated + // track also gets you download access, but purchasing a download-gated track + // only gets you download access (because the track was already free to stream). + const purchaseAccess = trackInfo?.is_stream_gated + ? PurchaseAccess.STREAM + : PurchaseAccess.DOWNLOAD if (!trackInfo || !isContentUSDCPurchaseGated(purchaseConditions)) { throw new Error('Content is missing purchase conditions') } @@ -105,7 +115,7 @@ function* getContentInfo({ contentId, contentType }: GetPurchaseConfigArgs) { const title = trackInfo.title const price = purchaseConditions.usdc_purchase.price - return { price, title, artistInfo, trackInfo } + return { price, title, artistInfo, purchaseAccess, trackInfo } } const getUserPurchaseMetadata = ({ @@ -261,6 +271,7 @@ type PurchaseWithCoinflowArgs = { purchaserUserId: ID /** USDC in dollars */ price: number + purchaseAccess: PurchaseAccess } function* purchaseWithCoinflow(args: PurchaseWithCoinflowArgs) { @@ -270,7 +281,8 @@ function* purchaseWithCoinflow(args: PurchaseWithCoinflowArgs) { splits, contentId, purchaserUserId, - price + price, + purchaseAccess } = args const audiusBackendInstance = yield* getContext('audiusBackendInstance') const feePayerAddress = yield* select(getFeePayer) @@ -292,7 +304,8 @@ function* purchaseWithCoinflow(args: PurchaseWithCoinflowArgs) { blocknumber, recentBlockhash, purchaserUserId, - wallet: rootAccount + wallet: rootAccount, + purchaseAccess } ) @@ -399,10 +412,13 @@ function* doStartPurchaseContentFlow({ const reportToSentry = yield* getContext('reportToSentry') const { track, make } = yield* getContext('analytics') - const { price, title, artistInfo } = yield* call(getContentInfo, { - contentId, - contentType - }) + const { price, title, artistInfo, purchaseAccess } = yield* call( + getContentInfo, + { + contentId, + contentType + } + ) const analyticsInfo = { price: price / 100, @@ -471,7 +487,8 @@ function* doStartPurchaseContentFlow({ extraAmount: extraAmountBN, splits, type: 'track', - purchaserUserId + purchaserUserId, + purchaseAccess }) } else { // We need to acquire USDC before the purchase can continue @@ -494,7 +511,8 @@ function* doStartPurchaseContentFlow({ contentId, contentType, purchaserUserId, - price: purchaseAmount + price: purchaseAmount, + purchaseAccess }) break case PurchaseVendor.STRIPE: @@ -506,7 +524,8 @@ function* doStartPurchaseContentFlow({ extraAmount: extraAmountBN, splits, type: 'track', - purchaserUserId + purchaserUserId, + purchaseAccess }) break } diff --git a/packages/discovery-provider/integration_tests/tasks/payment_router_mock_transactions.py b/packages/discovery-provider/integration_tests/tasks/payment_router_mock_transactions.py index cd3abc1c490..2a7a2e7f5a3 100644 --- a/packages/discovery-provider/integration_tests/tasks/payment_router_mock_transactions.py +++ b/packages/discovery-provider/integration_tests/tasks/payment_router_mock_transactions.py @@ -2973,3 +2973,596 @@ } ) ) + +mock_valid_track_purchase_stream_access = GetTransactionResp.from_json( + json.dumps( + { + "jsonrpc": "2.0", + "result": { + "slot": 190957, + "transaction": { + "signatures": [ + "5wPxiuLSF3MzXZt9XG99UEPNdxs8DtE2vWKezrB6zuMCrkMBJx6iU7xw5icaowpfgj96iLGnAgEAaBNSbneWdbZw" + ], + "message": { + "header": { + "numRequiredSignatures": 1, + "numReadonlySignedAccounts": 0, + "numReadonlyUnsignedAccounts": 5, + }, + "accountKeys": [ + "HXLN9UWwAjMPgHaFZDfgabT79SmLSdTeu2fUha2xHz9W", + "3XmVeZ6M1FYDdUQaNeQZf8dipvtzNP6NVb5xjDkdeiNb", + "A76eNhRrfdy6WfMoQf4ALasMxzRWHajH4TrVuX2NUjZT", + "7gfRGGdp89N9g3mCsZjaGmDDRdcTnZh9u3vYyBab2tRy", + "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + "26Q7gP8UfkDzi7GMFEQxTJaNJ8D2ybCUjex58M5MLu8y", + "Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo", + "apaySbqV1XAmuiGszeN4NyWrXkkMrnuJVoNhzmS1AMa", + "G231EZsMoCNBiQKP5quEeAM3oG516Zspirjnh7ywP71i", + ], + "recentBlockhash": "6D65tSU7pjSmFvSj9qK2W2bjkESw4XZebeNmgA1rCqnF", + "instructions": [ + { + "programIdIndex": 4, + "accounts": [1, 5, 2, 0], + "data": "hYECWfYe8vYqs", + "stackHeight": None, + }, + { + "programIdIndex": 6, + "accounts": [0], + # "track:1:1:2:stream" + "data": "5q8ftnvrq1ERw9QN7ZQv8xZPA", + "stackHeight": None, + }, + { + "programIdIndex": 7, + "accounts": [2, 8, 4, 3], + "data": "BQD4GnQPrhbq6Y9NJLnwDUziXhfF6BjkLYFbnKZH", + "stackHeight": None, + }, + ], + "addressTableLookups": [], + }, + }, + "meta": { + "err": None, + "status": {"Ok": None}, + "fee": 5000, + "preBalances": [ + 8420804160, + 2039280, + 2039280, + 2039280, + 929020800, + 1461600, + 119712000, + 1141440, + 946560, + ], + "postBalances": [ + 8420799160, + 2039280, + 2039280, + 2039280, + 929020800, + 1461600, + 119712000, + 1141440, + 946560, + ], + "innerInstructions": [ + { + "index": 2, + "instructions": [ + { + "programIdIndex": 4, + "accounts": [2, 3, 8, 8], + "data": "3YKuzAsyicvj", + "stackHeight": 2, + } + ], + } + ], + "logMessages": [ + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [1]", + "Program log: Instruction: TransferChecked", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 6173 of 600000 compute units", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", + "Program Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo invoke [1]", + "Program Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo consumed 480 of 593827 compute units", + "Program Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo success", + "Program apaySbqV1XAmuiGszeN4NyWrXkkMrnuJVoNhzmS1AMa invoke [1]", + "Program log: Instruction: Route", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]", + "Program log: Instruction: Transfer", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4728 of 576902 compute units", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", + "Program log: All transfers complete!", + "Program apaySbqV1XAmuiGszeN4NyWrXkkMrnuJVoNhzmS1AMa consumed 21782 of 593347 compute units", + "Program apaySbqV1XAmuiGszeN4NyWrXkkMrnuJVoNhzmS1AMa success", + ], + "preTokenBalances": [ + { + "accountIndex": 1, + "mint": "26Q7gP8UfkDzi7GMFEQxTJaNJ8D2ybCUjex58M5MLu8y", + "uiTokenAmount": { + "uiAmount": 10.0, + "decimals": 6, + "amount": "10000000", + "uiAmountString": "10.0", + }, + "owner": "HXLN9UWwAjMPgHaFZDfgabT79SmLSdTeu2fUha2xHz9W", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + }, + { + "accountIndex": 2, + "mint": "26Q7gP8UfkDzi7GMFEQxTJaNJ8D2ybCUjex58M5MLu8y", + "uiTokenAmount": { + "uiAmount": None, + "decimals": 6, + "amount": "0", + "uiAmountString": "0", + }, + "owner": "G231EZsMoCNBiQKP5quEeAM3oG516Zspirjnh7ywP71i", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + }, + { + "accountIndex": 3, + "mint": "26Q7gP8UfkDzi7GMFEQxTJaNJ8D2ybCUjex58M5MLu8y", + "uiTokenAmount": { + "uiAmount": 0, + "decimals": 6, + "amount": "0", + "uiAmountString": "0", + }, + "owner": "7vKR1WSmyHvBmCvKPZBiN66PHZqYQbXw51SZdwtVd9Dt", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + }, + ], + "postTokenBalances": [ + { + "accountIndex": 1, + "mint": "26Q7gP8UfkDzi7GMFEQxTJaNJ8D2ybCUjex58M5MLu8y", + "uiTokenAmount": { + "uiAmount": 9.0, + "decimals": 6, + "amount": "9000000", + "uiAmountString": "9.0", + }, + "owner": "HXLN9UWwAjMPgHaFZDfgabT79SmLSdTeu2fUha2xHz9W", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + }, + { + "accountIndex": 2, + "mint": "26Q7gP8UfkDzi7GMFEQxTJaNJ8D2ybCUjex58M5MLu8y", + "uiTokenAmount": { + "uiAmount": None, + "decimals": 6, + "amount": "0", + "uiAmountString": "0", + }, + "owner": "G231EZsMoCNBiQKP5quEeAM3oG516Zspirjnh7ywP71i", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + }, + { + "accountIndex": 3, + "mint": "26Q7gP8UfkDzi7GMFEQxTJaNJ8D2ybCUjex58M5MLu8y", + "uiTokenAmount": { + "uiAmount": 1.0, + "decimals": 6, + "amount": "1000000", + "uiAmountString": "1.0", + }, + "owner": "7vKR1WSmyHvBmCvKPZBiN66PHZqYQbXw51SZdwtVd9Dt", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + }, + ], + "rewards": [], + "loadedAddresses": {"writable": [], "readonly": []}, + "computeUnitsConsumed": 28435, + }, + "version": 0, + "blockTime": 1701922096, + }, + "id": 0, + } + ) +) + +# Valid purchase transaction specifying stream access purchase in memo +mock_valid_track_purchase_stream_access = GetTransactionResp.from_json( + json.dumps( + { + "jsonrpc": "2.0", + "result": { + "slot": 190957, + "transaction": { + "signatures": [ + "5wPxiuLSF3MzXZt9XG99UEPNdxs8DtE2vWKezrB6zuMCrkMBJx6iU7xw5icaowpfgj96iLGnAgEAaBNSbneWdbZw" + ], + "message": { + "header": { + "numRequiredSignatures": 1, + "numReadonlySignedAccounts": 0, + "numReadonlyUnsignedAccounts": 5, + }, + "accountKeys": [ + "HXLN9UWwAjMPgHaFZDfgabT79SmLSdTeu2fUha2xHz9W", + "3XmVeZ6M1FYDdUQaNeQZf8dipvtzNP6NVb5xjDkdeiNb", + "A76eNhRrfdy6WfMoQf4ALasMxzRWHajH4TrVuX2NUjZT", + "7gfRGGdp89N9g3mCsZjaGmDDRdcTnZh9u3vYyBab2tRy", + "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + "26Q7gP8UfkDzi7GMFEQxTJaNJ8D2ybCUjex58M5MLu8y", + "Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo", + "apaySbqV1XAmuiGszeN4NyWrXkkMrnuJVoNhzmS1AMa", + "G231EZsMoCNBiQKP5quEeAM3oG516Zspirjnh7ywP71i", + ], + "recentBlockhash": "6D65tSU7pjSmFvSj9qK2W2bjkESw4XZebeNmgA1rCqnF", + "instructions": [ + { + "programIdIndex": 4, + "accounts": [1, 5, 2, 0], + "data": "hYECWfYe8vYqs", + "stackHeight": None, + }, + { + "programIdIndex": 6, + "accounts": [0], + # "track:1:1:2:stream" + "data": "5q8ftnvrq1ERw9QN7ZQv8xZPA", + "stackHeight": None, + }, + { + "programIdIndex": 7, + "accounts": [2, 8, 4, 3], + "data": "BQD4GnQPrhbq6Y9NJLnwDUziXhfF6BjkLYFbnKZH", + "stackHeight": None, + }, + ], + "addressTableLookups": [], + }, + }, + "meta": { + "err": None, + "status": {"Ok": None}, + "fee": 5000, + "preBalances": [ + 8420804160, + 2039280, + 2039280, + 2039280, + 929020800, + 1461600, + 119712000, + 1141440, + 946560, + ], + "postBalances": [ + 8420799160, + 2039280, + 2039280, + 2039280, + 929020800, + 1461600, + 119712000, + 1141440, + 946560, + ], + "innerInstructions": [ + { + "index": 2, + "instructions": [ + { + "programIdIndex": 4, + "accounts": [2, 3, 8, 8], + "data": "3YKuzAsyicvj", + "stackHeight": 2, + } + ], + } + ], + "logMessages": [ + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [1]", + "Program log: Instruction: TransferChecked", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 6173 of 600000 compute units", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", + "Program Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo invoke [1]", + "Program Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo consumed 480 of 593827 compute units", + "Program Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo success", + "Program apaySbqV1XAmuiGszeN4NyWrXkkMrnuJVoNhzmS1AMa invoke [1]", + "Program log: Instruction: Route", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]", + "Program log: Instruction: Transfer", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4728 of 576902 compute units", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", + "Program log: All transfers complete!", + "Program apaySbqV1XAmuiGszeN4NyWrXkkMrnuJVoNhzmS1AMa consumed 21782 of 593347 compute units", + "Program apaySbqV1XAmuiGszeN4NyWrXkkMrnuJVoNhzmS1AMa success", + ], + "preTokenBalances": [ + { + "accountIndex": 1, + "mint": "26Q7gP8UfkDzi7GMFEQxTJaNJ8D2ybCUjex58M5MLu8y", + "uiTokenAmount": { + "uiAmount": 10.0, + "decimals": 6, + "amount": "10000000", + "uiAmountString": "10.0", + }, + "owner": "HXLN9UWwAjMPgHaFZDfgabT79SmLSdTeu2fUha2xHz9W", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + }, + { + "accountIndex": 2, + "mint": "26Q7gP8UfkDzi7GMFEQxTJaNJ8D2ybCUjex58M5MLu8y", + "uiTokenAmount": { + "uiAmount": None, + "decimals": 6, + "amount": "0", + "uiAmountString": "0", + }, + "owner": "G231EZsMoCNBiQKP5quEeAM3oG516Zspirjnh7ywP71i", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + }, + { + "accountIndex": 3, + "mint": "26Q7gP8UfkDzi7GMFEQxTJaNJ8D2ybCUjex58M5MLu8y", + "uiTokenAmount": { + "uiAmount": 0, + "decimals": 6, + "amount": "0", + "uiAmountString": "0", + }, + "owner": "7vKR1WSmyHvBmCvKPZBiN66PHZqYQbXw51SZdwtVd9Dt", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + }, + ], + "postTokenBalances": [ + { + "accountIndex": 1, + "mint": "26Q7gP8UfkDzi7GMFEQxTJaNJ8D2ybCUjex58M5MLu8y", + "uiTokenAmount": { + "uiAmount": 9.0, + "decimals": 6, + "amount": "9000000", + "uiAmountString": "9.0", + }, + "owner": "HXLN9UWwAjMPgHaFZDfgabT79SmLSdTeu2fUha2xHz9W", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + }, + { + "accountIndex": 2, + "mint": "26Q7gP8UfkDzi7GMFEQxTJaNJ8D2ybCUjex58M5MLu8y", + "uiTokenAmount": { + "uiAmount": None, + "decimals": 6, + "amount": "0", + "uiAmountString": "0", + }, + "owner": "G231EZsMoCNBiQKP5quEeAM3oG516Zspirjnh7ywP71i", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + }, + { + "accountIndex": 3, + "mint": "26Q7gP8UfkDzi7GMFEQxTJaNJ8D2ybCUjex58M5MLu8y", + "uiTokenAmount": { + "uiAmount": 1.0, + "decimals": 6, + "amount": "1000000", + "uiAmountString": "1.0", + }, + "owner": "7vKR1WSmyHvBmCvKPZBiN66PHZqYQbXw51SZdwtVd9Dt", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + }, + ], + "rewards": [], + "loadedAddresses": {"writable": [], "readonly": []}, + "computeUnitsConsumed": 28435, + }, + "version": 0, + "blockTime": 1701922096, + }, + "id": 0, + } + ) +) + +# Valid purchase transaction specifying download access purchase in memo +mock_valid_track_purchase_download_access = GetTransactionResp.from_json( + json.dumps( + { + "jsonrpc": "2.0", + "result": { + "slot": 190957, + "transaction": { + "signatures": [ + "5wPxiuLSF3MzXZt9XG99UEPNdxs8DtE2vWKezrB6zuMCrkMBJx6iU7xw5icaowpfgj96iLGnAgEAaBNSbneWdbZY" + ], + "message": { + "header": { + "numRequiredSignatures": 1, + "numReadonlySignedAccounts": 0, + "numReadonlyUnsignedAccounts": 5, + }, + "accountKeys": [ + "HXLN9UWwAjMPgHaFZDfgabT79SmLSdTeu2fUha2xHz9W", + "3XmVeZ6M1FYDdUQaNeQZf8dipvtzNP6NVb5xjDkdeiNb", + "A76eNhRrfdy6WfMoQf4ALasMxzRWHajH4TrVuX2NUjZT", + "7gfRGGdp89N9g3mCsZjaGmDDRdcTnZh9u3vYyBab2tRy", + "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + "26Q7gP8UfkDzi7GMFEQxTJaNJ8D2ybCUjex58M5MLu8y", + "Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo", + "apaySbqV1XAmuiGszeN4NyWrXkkMrnuJVoNhzmS1AMa", + "G231EZsMoCNBiQKP5quEeAM3oG516Zspirjnh7ywP71i", + ], + "recentBlockhash": "6D65tSU7pjSmFvSj9qK2W2bjkESw4XZebeNmgA1rCqnF", + "instructions": [ + { + "programIdIndex": 4, + "accounts": [1, 5, 2, 0], + "data": "hYECWfYe8vYqs", + "stackHeight": None, + }, + { + "programIdIndex": 6, + "accounts": [0], + # "track:3:1:2:download" + "data": "2d6R8DNFA5Yr5TTvcCbhfZ1q2eY3", + "stackHeight": None, + }, + { + "programIdIndex": 7, + "accounts": [2, 8, 4, 3], + "data": "BQD4GnQPrhbq6Y9NJLnwDUziXhfF6BjkLYFbnKZH", + "stackHeight": None, + }, + ], + "addressTableLookups": [], + }, + }, + "meta": { + "err": None, + "status": {"Ok": None}, + "fee": 5000, + "preBalances": [ + 8420804160, + 2039280, + 2039280, + 2039280, + 929020800, + 1461600, + 119712000, + 1141440, + 946560, + ], + "postBalances": [ + 8420799160, + 2039280, + 2039280, + 2039280, + 929020800, + 1461600, + 119712000, + 1141440, + 946560, + ], + "innerInstructions": [ + { + "index": 2, + "instructions": [ + { + "programIdIndex": 4, + "accounts": [2, 3, 8, 8], + "data": "3YKuzAsyicvj", + "stackHeight": 2, + } + ], + } + ], + "logMessages": [ + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [1]", + "Program log: Instruction: TransferChecked", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 6173 of 600000 compute units", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", + "Program Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo invoke [1]", + "Program Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo consumed 480 of 593827 compute units", + "Program Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo success", + "Program apaySbqV1XAmuiGszeN4NyWrXkkMrnuJVoNhzmS1AMa invoke [1]", + "Program log: Instruction: Route", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]", + "Program log: Instruction: Transfer", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4728 of 576902 compute units", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", + "Program log: All transfers complete!", + "Program apaySbqV1XAmuiGszeN4NyWrXkkMrnuJVoNhzmS1AMa consumed 21782 of 593347 compute units", + "Program apaySbqV1XAmuiGszeN4NyWrXkkMrnuJVoNhzmS1AMa success", + ], + "preTokenBalances": [ + { + "accountIndex": 1, + "mint": "26Q7gP8UfkDzi7GMFEQxTJaNJ8D2ybCUjex58M5MLu8y", + "uiTokenAmount": { + "uiAmount": 10.0, + "decimals": 6, + "amount": "10000000", + "uiAmountString": "10.0", + }, + "owner": "HXLN9UWwAjMPgHaFZDfgabT79SmLSdTeu2fUha2xHz9W", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + }, + { + "accountIndex": 2, + "mint": "26Q7gP8UfkDzi7GMFEQxTJaNJ8D2ybCUjex58M5MLu8y", + "uiTokenAmount": { + "uiAmount": None, + "decimals": 6, + "amount": "0", + "uiAmountString": "0", + }, + "owner": "G231EZsMoCNBiQKP5quEeAM3oG516Zspirjnh7ywP71i", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + }, + { + "accountIndex": 3, + "mint": "26Q7gP8UfkDzi7GMFEQxTJaNJ8D2ybCUjex58M5MLu8y", + "uiTokenAmount": { + "uiAmount": 0, + "decimals": 6, + "amount": "0", + "uiAmountString": "0", + }, + "owner": "7vKR1WSmyHvBmCvKPZBiN66PHZqYQbXw51SZdwtVd9Dt", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + }, + ], + "postTokenBalances": [ + { + "accountIndex": 1, + "mint": "26Q7gP8UfkDzi7GMFEQxTJaNJ8D2ybCUjex58M5MLu8y", + "uiTokenAmount": { + "uiAmount": 9.0, + "decimals": 6, + "amount": "9000000", + "uiAmountString": "9.0", + }, + "owner": "HXLN9UWwAjMPgHaFZDfgabT79SmLSdTeu2fUha2xHz9W", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + }, + { + "accountIndex": 2, + "mint": "26Q7gP8UfkDzi7GMFEQxTJaNJ8D2ybCUjex58M5MLu8y", + "uiTokenAmount": { + "uiAmount": None, + "decimals": 6, + "amount": "0", + "uiAmountString": "0", + }, + "owner": "G231EZsMoCNBiQKP5quEeAM3oG516Zspirjnh7ywP71i", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + }, + { + "accountIndex": 3, + "mint": "26Q7gP8UfkDzi7GMFEQxTJaNJ8D2ybCUjex58M5MLu8y", + "uiTokenAmount": { + "uiAmount": 1.0, + "decimals": 6, + "amount": "1000000", + "uiAmountString": "1.0", + }, + "owner": "7vKR1WSmyHvBmCvKPZBiN66PHZqYQbXw51SZdwtVd9Dt", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + }, + ], + "rewards": [], + "loadedAddresses": {"writable": [], "readonly": []}, + "computeUnitsConsumed": 28435, + }, + "version": 0, + "blockTime": 1701922096, + }, + "id": 0, + } + ) +) diff --git a/packages/discovery-provider/integration_tests/tasks/test_index_payment_router.py b/packages/discovery-provider/integration_tests/tasks/test_index_payment_router.py index b768aff1aa0..58c892a41fa 100644 --- a/packages/discovery-provider/integration_tests/tasks/test_index_payment_router.py +++ b/packages/discovery-provider/integration_tests/tasks/test_index_payment_router.py @@ -7,11 +7,13 @@ mock_invalid_track_purchase_insufficient_split_tx, mock_invalid_track_purchase_missing_split_tx, mock_non_route_transfer_purchase_single_recipient_tx, + mock_valid_track_purchase_download_access, mock_valid_track_purchase_from_user_bank_single_recipient_tx, mock_valid_track_purchase_multi_recipient_pay_extra_tx, mock_valid_track_purchase_multi_recipient_tx, mock_valid_track_purchase_single_recipient_pay_extra_tx, mock_valid_track_purchase_single_recipient_tx, + mock_valid_track_purchase_stream_access, mock_valid_transfer_from_user_bank_without_purchase_single_recipient_tx, mock_valid_transfer_single_recipient_recovery_tx, mock_valid_transfer_without_purchase_multi_recipient_tx, @@ -81,6 +83,7 @@ "tracks": [ {"track_id": 1, "title": "track 1", "owner_id": 1}, {"track_id": 2, "title": "track 2", "owner_id": 1}, + {"track_id": 3, "title": "track 3", "owner_id": 1}, ], "track_price_history": [ { # pay full price to trackOwner @@ -96,7 +99,12 @@ "7dw7W4Yv7F1uWb9dVH1CFPm39mePyypuCji2zxcFA556": 1000000, }, "total_price_cents": 200, - "access": PurchaseAccessType.stream, + }, + { # download access type + "track_id": 3, + "splits": {"7gfRGGdp89N9g3mCsZjaGmDDRdcTnZh9u3vYyBab2tRy": 1000000}, + "total_price_cents": 100, + "access": PurchaseAccessType.download, }, ], } @@ -916,3 +924,63 @@ def test_process_payment_router_txs_details_skip_unknown_PDA_ATAs(app): .first() ) assert transaction_record is None + + +# Index tx with stream access in memo correctly +def test_process_payment_router_tx_details_stream_access(app): + with app.app_context(): + db = get_db() + populate_mock_db(db, test_entries) + challenge_event_bus = create_autospec(ChallengeEventBus) + + tx_response = mock_valid_track_purchase_stream_access + transaction = tx_response.value.transaction.transaction + tx_sig_str = str(transaction.signatures[0]) + + with db.scoped_session() as session: + process_payment_router_tx_details( + session=session, + tx_info=tx_response, + tx_sig=tx_sig_str, + timestamp=datetime.now(), + challenge_event_bus=challenge_event_bus, + ) + + purchase = ( + session.query(USDCPurchase) + .filter(USDCPurchase.signature == tx_sig_str) + .first() + ) + + assert purchase is not None + assert purchase.access == PurchaseAccessType.stream + + +# Index tx with download access in memo correctly +def test_process_payment_router_tx_details_download_access(app): + with app.app_context(): + db = get_db() + populate_mock_db(db, test_entries) + challenge_event_bus = create_autospec(ChallengeEventBus) + + tx_response = mock_valid_track_purchase_download_access + transaction = tx_response.value.transaction.transaction + tx_sig_str = str(transaction.signatures[0]) + + with db.scoped_session() as session: + process_payment_router_tx_details( + session=session, + tx_info=tx_response, + tx_sig=tx_sig_str, + timestamp=datetime.now(), + challenge_event_bus=challenge_event_bus, + ) + + purchase = ( + session.query(USDCPurchase) + .filter(USDCPurchase.signature == tx_sig_str) + .first() + ) + + assert purchase is not None + assert purchase.access == PurchaseAccessType.download diff --git a/packages/discovery-provider/integration_tests/tasks/test_index_user_bank.py b/packages/discovery-provider/integration_tests/tasks/test_index_user_bank.py index 8dc9798999f..8c7b0a7d8a8 100644 --- a/packages/discovery-provider/integration_tests/tasks/test_index_user_bank.py +++ b/packages/discovery-provider/integration_tests/tasks/test_index_user_bank.py @@ -20,6 +20,8 @@ mock_valid_create_usdc_token_account_tx, mock_valid_track_purchase_pay_extra_tx, mock_valid_track_purchase_tx, + mock_valid_track_purchase_tx_download_access, + mock_valid_track_purchase_tx_stream_access, mock_valid_transfer_prepare_withdrawal_tx, mock_valid_transfer_withdrawal_tx, mock_valid_transfer_without_purchase_tx, @@ -123,7 +125,6 @@ "track_id": 1, "splits": {RECIPIENT_USDC_USER_BANK_ADDRESS: 1000000}, "total_price_cents": 100, - "access": PurchaseAccessType.stream, }, { # pay $1 each to track owner and third party "track_id": 2, @@ -132,7 +133,12 @@ EXTERNAL_ACCOUNT_ADDRESS: 1000000, }, "total_price_cents": 200, - "access": PurchaseAccessType.stream, + }, + { # download access type + "track_id": 3, + "splits": {RECIPIENT_USDC_USER_BANK_ADDRESS: 1000000}, + "total_price_cents": 100, + "access": PurchaseAccessType.download, }, ], } @@ -177,6 +183,7 @@ def test_process_user_bank_tx_details_valid_purchase(app): assert purchase.extra_amount == 0 assert purchase.content_type == PurchaseType.track assert purchase.content_id == 1 + assert purchase.access == PurchaseAccessType.stream owner_transaction_record = ( session.query(USDCTransactionsHistory) @@ -997,3 +1004,69 @@ def test_process_user_bank_txs_details_transfer_audio_tip_challenge_event(app): calls = [call(ChallengeEvent.send_tip, tx_response.value.slot, sender_user_id)] challenge_event_bus.dispatch.assert_has_calls(calls) + + +# Index tx with stream access in memo correctly +def test_process_user_bank_txs_details_stream_access(app): + with app.app_context(): + db = get_db() + redis = get_redis() + solana_client_manager_mock = create_autospec(SolanaClientManager) + challenge_event_bus = create_autospec(ChallengeEventBus) + populate_mock_db(db, test_entries) + + tx_response = mock_valid_track_purchase_tx_stream_access + transaction = tx_response.value.transaction.transaction + tx_sig_str = str(transaction.signatures[0]) + + with db.scoped_session() as session: + process_user_bank_tx_details( + solana_client_manager=solana_client_manager_mock, + session=session, + redis=redis, + tx_info=tx_response, + tx_sig=tx_sig_str, + timestamp=datetime.now(), + challenge_event_bus=challenge_event_bus, + ) + + purchase = ( + session.query(USDCPurchase) + .filter(USDCPurchase.signature == tx_sig_str) + .first() + ) + assert purchase is not None + assert purchase.access == PurchaseAccessType.stream + + +# Index tx with download access in memo correctly +def test_process_user_bank_txs_details_download_access(app): + with app.app_context(): + db = get_db() + redis = get_redis() + solana_client_manager_mock = create_autospec(SolanaClientManager) + challenge_event_bus = create_autospec(ChallengeEventBus) + populate_mock_db(db, test_entries) + + tx_response = mock_valid_track_purchase_tx_download_access + transaction = tx_response.value.transaction.transaction + tx_sig_str = str(transaction.signatures[0]) + + with db.scoped_session() as session: + process_user_bank_tx_details( + solana_client_manager=solana_client_manager_mock, + session=session, + redis=redis, + tx_info=tx_response, + tx_sig=tx_sig_str, + timestamp=datetime.now(), + challenge_event_bus=challenge_event_bus, + ) + + purchase = ( + session.query(USDCPurchase) + .filter(USDCPurchase.signature == tx_sig_str) + .first() + ) + assert purchase is not None + assert purchase.access == PurchaseAccessType.download diff --git a/packages/discovery-provider/integration_tests/tasks/user_bank_mock_transactions.py b/packages/discovery-provider/integration_tests/tasks/user_bank_mock_transactions.py index b34dc2592e3..30c5ea206bf 100644 --- a/packages/discovery-provider/integration_tests/tasks/user_bank_mock_transactions.py +++ b/packages/discovery-provider/integration_tests/tasks/user_bank_mock_transactions.py @@ -56,6 +56,12 @@ PURCHASE_TRACK1_MEMO_DATA = "7YSwHDhdZsHu6X" # base58.b58encode("track:2:10").decode("utf-8") PURCHASE_TRACK2_MEMO_DATA = "7YSwHDhdZtmtNs" +# base58.b58encode("track:1:10:2:stream").decode("utf-8") +PURCHASE_TRACK_STREAM_ACCESS_MEMO_DATA = "NKSrfbiivt2H3Rc3uQd2JbGcAY" +# base58.b58encode("track:3:10:2:download").decode("utf-8") +PURCHASE_TRACK_DOWNLOAD_MEMO_DATA = "8AJuUqamV55ZcUtzu1FNWfHj9WwVm" + + # base58.b58encode("Prepare Withdrawal").decode("utf-8") PREPARE_WITHDRAWAL_MEMO = "4LXeTxmZydvvx9jk2DnmBAwcX" # base58.b58encode("Withdrawal").decode("utf-8") @@ -412,7 +418,6 @@ ) ) - # Transfer $1 USDC between two user banks without a purchase mock_valid_transfer_without_purchase_tx = GetTransactionResp.from_json( json.dumps( @@ -2443,3 +2448,349 @@ } ) ) + +# Valid purchase transaction with "stream" access in memo +mock_valid_track_purchase_tx_stream_access = GetTransactionResp.from_json( + json.dumps( + { + "jsonrpc": "2.0", + "result": { + "slot": 227246439, + "transaction": { + "signatures": [MOCK_SIGNATURE], + "message": { + "header": { + "numRequiredSignatures": 1, + "numReadonlySignedAccounts": 0, + "numReadonlyUnsignedAccounts": 8, + }, + "accountKeys": [ + FEE_PAYER, + SENDER_USDC_USER_BANK_ADDRESS, + NONCE_ACCOUNT_ADDRESS, + RECIPIENT_USDC_USER_BANK_ADDRESS, + "11111111111111111111111111111111", + CLAIMABLE_TOKENS_PDA, + USDC_PDA, + "KeccakSecp256k11111111111111111111111111111", + "Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo", + "Sysvar1nstructions1111111111111111111111111", + "SysvarRent111111111111111111111111111111111", + "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + ], + "recentBlockhash": "5H434VMiHgK7RaJZaBKKcriu4eky8erb9QGfcHJSZquU", + "instructions": [ + { + "programIdIndex": 7, + "accounts": [], + "data": "H4eCheRWTZDTCFYUcyMzE6EhQMZvvvLKJ9g6YaUpbZeoLLgVj1uvwCTdzcb2MzbKHsRjN8DjLYdqxuQEZe2TjUKCuBMrFtpnnLd4RcvBnr4ieHCdH8ZU1N6XDfiqyKB4zenQ9S4viza4ob4gbtmiRS6o6KGEtL3fJQRvaA3tdtSx1rfFogZzwMXAxHrkuxHrpAqfm", + "stackHeight": None, + }, + { + "programIdIndex": 5, + "accounts": [0, 1, 3, 2, 6, 10, 9, 4, 11], + "data": "6dMrrkPeSzw2r5huQ6RToaJCaVuu", + "stackHeight": None, + }, + { + "programIdIndex": 8, + "accounts": [0], + "data": PURCHASE_TRACK_STREAM_ACCESS_MEMO_DATA, + "stackHeight": None, + }, + ], + }, + }, + "meta": { + "err": None, + "status": {"Ok": None}, + "fee": 10000, + "preBalances": [ + 1689358166, + 2039280, + 953520, + 2039280, + 1, + 1141440, + 0, + 1, + 121159680, + 0, + 1009200, + 934087680, + ], + "postBalances": [ + 1689348166, + 2039280, + 953520, + 2039280, + 1, + 1141440, + 0, + 1, + 121159680, + 0, + 1009200, + 934087680, + ], + "innerInstructions": [ + { + "index": 1, + "instructions": [ + { + "programIdIndex": 11, + "accounts": [1, 3, 6, 6], + "data": "3mhiKuxuaKy1", + "stackHeight": 2, + } + ], + } + ], + "logMessages": [ + f"Program {CLAIMABLE_TOKENS_PDA} invoke [1]", + "Program log: Instruction: Transfer", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]", + "Program log: Instruction: Transfer", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4728 of 581084 compute units", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", + f"Program {CLAIMABLE_TOKENS_PDA} consumed 24149 of 600000 compute units", + f"Program {CLAIMABLE_TOKENS_PDA} success", + "Program Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo invoke [1]", + "Program Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo consumed 588 of 575851 compute units", + "Program Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo success", + ], + "preTokenBalances": [ + { + "accountIndex": 1, + "mint": USDC_MINT, + "uiTokenAmount": { + "uiAmount": 1.0, + "decimals": 6, + "amount": "1000000", + "uiAmountString": "1", + }, + "owner": USDC_PDA, + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + }, + { + "accountIndex": 3, + "mint": USDC_MINT, + "uiTokenAmount": { + "uiAmount": None, + "decimals": 6, + "amount": "0", + "uiAmountString": "0", + }, + "owner": USDC_PDA, + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + }, + ], + "postTokenBalances": [ + { + "accountIndex": 1, + "mint": USDC_MINT, + "uiTokenAmount": { + "uiAmount": None, + "decimals": 6, + "amount": "0", + "uiAmountString": "0", + }, + "owner": USDC_PDA, + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + }, + { + "accountIndex": 3, + "mint": USDC_MINT, + "uiTokenAmount": { + "uiAmount": 1.0, + "decimals": 6, + "amount": "1000000", + "uiAmountString": "1", + }, + "owner": USDC_PDA, + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + }, + ], + "rewards": [], + "loadedAddresses": {"writable": [], "readonly": []}, + "computeUnitsConsumed": 24737, + }, + "blockTime": 1698802811, + }, + "id": 0, + } + ) +) + +# Valid purchase transaction with "download" access in memo +mock_valid_track_purchase_tx_download_access = GetTransactionResp.from_json( + json.dumps( + { + "jsonrpc": "2.0", + "result": { + "slot": 227246439, + "transaction": { + "signatures": [MOCK_SIGNATURE], + "message": { + "header": { + "numRequiredSignatures": 1, + "numReadonlySignedAccounts": 0, + "numReadonlyUnsignedAccounts": 8, + }, + "accountKeys": [ + FEE_PAYER, + SENDER_USDC_USER_BANK_ADDRESS, + NONCE_ACCOUNT_ADDRESS, + RECIPIENT_USDC_USER_BANK_ADDRESS, + "11111111111111111111111111111111", + CLAIMABLE_TOKENS_PDA, + USDC_PDA, + "KeccakSecp256k11111111111111111111111111111", + "Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo", + "Sysvar1nstructions1111111111111111111111111", + "SysvarRent111111111111111111111111111111111", + "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + ], + "recentBlockhash": "5H434VMiHgK7RaJZaBKKcriu4eky8erb9QGfcHJSZquU", + "instructions": [ + { + "programIdIndex": 7, + "accounts": [], + "data": "H4eCheRWTZDTCFYUcyMzE6EhQMZvvvLKJ9g6YaUpbZeoLLgVj1uvwCTdzcb2MzbKHsRjN8DjLYdqxuQEZe2TjUKCuBMrFtpnnLd4RcvBnr4ieHCdH8ZU1N6XDfiqyKB4zenQ9S4viza4ob4gbtmiRS6o6KGEtL3fJQRvaA3tdtSx1rfFogZzwMXAxHrkuxHrpAqfm", + "stackHeight": None, + }, + { + "programIdIndex": 5, + "accounts": [0, 1, 3, 2, 6, 10, 9, 4, 11], + "data": "6dMrrkPeSzw2r5huQ6RToaJCaVuu", + "stackHeight": None, + }, + { + "programIdIndex": 8, + "accounts": [0], + "data": PURCHASE_TRACK_DOWNLOAD_MEMO_DATA, + "stackHeight": None, + }, + ], + }, + }, + "meta": { + "err": None, + "status": {"Ok": None}, + "fee": 10000, + "preBalances": [ + 1689358166, + 2039280, + 953520, + 2039280, + 1, + 1141440, + 0, + 1, + 121159680, + 0, + 1009200, + 934087680, + ], + "postBalances": [ + 1689348166, + 2039280, + 953520, + 2039280, + 1, + 1141440, + 0, + 1, + 121159680, + 0, + 1009200, + 934087680, + ], + "innerInstructions": [ + { + "index": 1, + "instructions": [ + { + "programIdIndex": 11, + "accounts": [1, 3, 6, 6], + "data": "3mhiKuxuaKy1", + "stackHeight": 2, + } + ], + } + ], + "logMessages": [ + f"Program {CLAIMABLE_TOKENS_PDA} invoke [1]", + "Program log: Instruction: Transfer", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]", + "Program log: Instruction: Transfer", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4728 of 581084 compute units", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", + f"Program {CLAIMABLE_TOKENS_PDA} consumed 24149 of 600000 compute units", + f"Program {CLAIMABLE_TOKENS_PDA} success", + "Program Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo invoke [1]", + "Program Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo consumed 588 of 575851 compute units", + "Program Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo success", + ], + "preTokenBalances": [ + { + "accountIndex": 1, + "mint": USDC_MINT, + "uiTokenAmount": { + "uiAmount": 1.0, + "decimals": 6, + "amount": "1000000", + "uiAmountString": "1", + }, + "owner": USDC_PDA, + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + }, + { + "accountIndex": 3, + "mint": USDC_MINT, + "uiTokenAmount": { + "uiAmount": None, + "decimals": 6, + "amount": "0", + "uiAmountString": "0", + }, + "owner": USDC_PDA, + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + }, + ], + "postTokenBalances": [ + { + "accountIndex": 1, + "mint": USDC_MINT, + "uiTokenAmount": { + "uiAmount": None, + "decimals": 6, + "amount": "0", + "uiAmountString": "0", + }, + "owner": USDC_PDA, + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + }, + { + "accountIndex": 3, + "mint": USDC_MINT, + "uiTokenAmount": { + "uiAmount": 1.0, + "decimals": 6, + "amount": "1000000", + "uiAmountString": "1", + }, + "owner": USDC_PDA, + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + }, + ], + "rewards": [], + "loadedAddresses": {"writable": [], "readonly": []}, + "computeUnitsConsumed": 24737, + }, + "blockTime": 1698802811, + }, + "id": 0, + } + ) +) diff --git a/packages/discovery-provider/src/tasks/index_payment_router.py b/packages/discovery-provider/src/tasks/index_payment_router.py index 4a6f17af9da..ad242e0b15b 100644 --- a/packages/discovery-provider/src/tasks/index_payment_router.py +++ b/packages/discovery-provider/src/tasks/index_payment_router.py @@ -129,6 +129,7 @@ class PurchaseMetadataDict(TypedDict): id: int purchaser_user_id: int content_owner_id: int + access: PurchaseAccessType class RouteTransactionMemoType(str, enum.Enum): @@ -221,72 +222,83 @@ def parse_route_transaction_memo( blocknumber_str, purchaser_user_id_str, ) = content_metadata - type = PurchaseType[type_str.lower()] - id = int(id_str) - purchaser_user_id = int(purchaser_user_id_str) - blocknumber = int(blocknumber_str) - - # 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}" + access_str = "stream" # Default to stream access + elif len(content_metadata) == 5: + ( + type_str, + id_str, + blocknumber_str, + purchaser_user_id_str, + access_str, + ) = content_metadata + else: + logger.info( + f"index_payment_router.py | Ignoring memo, no content metadata found: {memo}" ) - price = None - splits = None - content_owner_id = None - if type == PurchaseType.track: - env = shared_config["discprov"]["env"] - content_owner_id = get_track_owner_id(session, id) - if content_owner_id is None: - logger.error( - f"index_payment_router.py | Couldn't find content owner for track_id={id}" - ) - continue - query = session.query(TrackPriceHistory) - if env != "dev": - # In local stack, the blocktime of solana-test-validator is offset. - # The start time of the validator is baked into the prebuilt container. - # So if the container was built on 7/15, but you upped the container on 7/22, the blocktimes will still say 7/15 and be way behind. - # To remedy this locally would require getting the start time of the solana-test-validator container and getting its offset compared to when - # the the validator thinks the beginning of time is, and that's just too much work so I'm just not adding the blocktime filter in local dev - query.filter(TrackPriceHistory.block_timestamp < timestamp) - result = ( - query.filter( - TrackPriceHistory.track_id == id, - ) - .order_by(desc(TrackPriceHistory.block_timestamp)) - .first() - ) - if result is not None: - price = result.total_price_cents - splits = result.splits - else: + + type = PurchaseType[type_str.lower()] + id = int(id_str) + purchaser_user_id = int(purchaser_user_id_str) + blocknumber = int(blocknumber_str) + access = PurchaseAccessType[access_str.lower()] + + # 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}" + ) + price = None + splits = None + content_owner_id = None + if type == PurchaseType.track: + env = shared_config["discprov"]["env"] + content_owner_id = get_track_owner_id(session, id) + if content_owner_id is None: logger.error( - f"index_payment_router.py | Unknown content type {type}" - ) - if ( - price is not None - and splits is not None - and isinstance(splits, dict) - and content_owner_id is not None - ): - return RouteTransactionMemo( - type=RouteTransactionMemoType.purchase, - metadata={ - "type": type, - "id": id, - "price": price * USDC_PER_USD_CENT, - "splits": splits, - "purchaser_user_id": purchaser_user_id, - "content_owner_id": content_owner_id, - }, + f"index_payment_router.py | Couldn't find content owner for track_id={id}" ) - else: - logger.error( - f"index_payment_router.py | Couldn't find relevant price for {content_metadata}" + continue + query = session.query(TrackPriceHistory) + if env != "dev": + # In local stack, the blocktime of solana-test-validator is offset. + # The start time of the validator is baked into the prebuilt container. + # So if the container was built on 7/15, but you upped the container on 7/22, the blocktimes will still say 7/15 and be way behind. + # To remedy this locally would require getting the start time of the solana-test-validator container and getting its offset compared to when + # the the validator thinks the beginning of time is, and that's just too much work so I'm just not adding the blocktime filter in local dev + query.filter(TrackPriceHistory.block_timestamp < timestamp) + result = ( + query.filter( + TrackPriceHistory.track_id == id, + TrackPriceHistory.access == access, ) + .order_by(desc(TrackPriceHistory.block_timestamp)) + .first() + ) + if result is not None: + price = result.total_price_cents + splits = result.splits else: - logger.info( - f"index_payment_router.py | Ignoring memo, no content metadata found: {memo}" + logger.error(f"index_payment_router.py | Unknown content type {type}") + if ( + price is not None + and splits is not None + and isinstance(splits, dict) + and content_owner_id is not None + ): + return RouteTransactionMemo( + type=RouteTransactionMemoType.purchase, + metadata={ + "type": type, + "id": id, + "price": price * USDC_PER_USD_CENT, + "splits": splits, + "purchaser_user_id": purchaser_user_id, + "content_owner_id": content_owner_id, + "access": access, + }, + ) + else: + logger.error( + f"index_payment_router.py | Couldn't find relevant price for {content_metadata}" ) except (ValueError, KeyError) as e: logger.info( @@ -341,7 +353,7 @@ def index_purchase( extra_amount=extra_amount, content_type=purchase_metadata["type"], content_id=purchase_metadata["id"], - access=PurchaseAccessType.stream, + access=purchase_metadata["access"], ) logger.debug( f"index_payment_router.py | tx: {tx_sig} | Creating usdc_purchase for purchase {usdc_purchase}" diff --git a/packages/discovery-provider/src/tasks/index_user_bank.py b/packages/discovery-provider/src/tasks/index_user_bank.py index 3e65aa237e9..e5848538d13 100644 --- a/packages/discovery-provider/src/tasks/index_user_bank.py +++ b/packages/discovery-provider/src/tasks/index_user_bank.py @@ -282,6 +282,7 @@ class PurchaseMetadataDict(TypedDict): type: PurchaseType id: int purchaser_user_id: Optional[int] + access: PurchaseAccessType def get_purchase_metadata_from_memo( @@ -294,6 +295,7 @@ def get_purchase_metadata_from_memo( if len(content_metadata) == 3: type_str, id_str, blocknumber_str = content_metadata purchaser_user_id_str = None + access_str = "stream" # default to stream access elif len(content_metadata) == 4: ( type_str, @@ -301,6 +303,15 @@ def get_purchase_metadata_from_memo( blocknumber_str, purchaser_user_id_str, ) = content_metadata + access_str = "stream" # default to stream access + elif len(content_metadata) == 5: + ( + type_str, + id_str, + blocknumber_str, + purchaser_user_id_str, + access_str, + ) = content_metadata else: logger.debug( f"index_user_bank.py | Ignoring memo, no content metadata found: {memo}" @@ -313,10 +324,11 @@ def get_purchase_metadata_from_memo( purchaser_user_id = ( int(purchaser_user_id_str) if purchaser_user_id_str else None ) + access = PurchaseAccessType[access_str.lower()] # TODO: Wait for blocknumber to be indexed by ACDC logger.debug( - f"index_user_bank.py | Found content_metadata in memo: type={type}, id={id}, blocknumber={blocknumber}, purchaser_user_id={purchaser_user_id}" + f"index_user_bank.py | Found content_metadata in memo: type={type}, id={id}, blocknumber={blocknumber}, purchaser_user_id={purchaser_user_id}, access={access}" ) price = None @@ -334,6 +346,7 @@ def get_purchase_metadata_from_memo( result = ( query.filter( TrackPriceHistory.track_id == id, + TrackPriceHistory.access == access, ) .order_by(desc(TrackPriceHistory.block_timestamp)) .first() @@ -352,6 +365,7 @@ def get_purchase_metadata_from_memo( "price": price * USDC_PER_USD_CENT, "splits": splits, "purchaser_user_id": purchaser_user_id, + "access": access, } logger.info( f"index_user_bank.py | Got purchase metadata {content_metadata}" @@ -416,7 +430,7 @@ def index_purchase( extra_amount=extra_amount, content_type=purchase_metadata["type"], content_id=purchase_metadata["id"], - access=PurchaseAccessType.stream, + access=purchase_metadata["access"], ) logger.debug( f"index_user_bank.py | Creating usdc_purchase for purchase {usdc_purchase}" diff --git a/packages/libs/src/services/solana/SolanaWeb3Manager.ts b/packages/libs/src/services/solana/SolanaWeb3Manager.ts index 4bf70405f78..85916214927 100644 --- a/packages/libs/src/services/solana/SolanaWeb3Manager.ts +++ b/packages/libs/src/services/solana/SolanaWeb3Manager.ts @@ -69,6 +69,7 @@ type CreateSenderParams = Omit< export type MintName = 'usdc' | 'audio' export const DEFAULT_MINT: MintName = 'audio' +export type PurchaseAccess = 'stream' | 'download' const MEMO_PROGRAM_ID = new PublicKey( 'Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo' @@ -521,7 +522,8 @@ export class SolanaWeb3Manager { blocknumber, extraAmount = 0, splits, - purchaserUserId + purchaserUserId, + purchaseAccess }: { id: number type: 'track' @@ -529,15 +531,13 @@ export class SolanaWeb3Manager { extraAmount?: number | BN blocknumber: number purchaserUserId: number + purchaseAccess: PurchaseAccess }) { if (!this.web3Manager) { throw new Error( 'A web3Manager is required for this solanaWeb3Manager method' ) } - if (!splits) { - throw new Error('Splits must be provided') - } if (Object.values(splits).length !== 1) { throw new Error( 'Purchasing content only supports a single split. Specifying more splits coming soon!' @@ -571,7 +571,7 @@ export class SolanaWeb3Manager { mintKey: this.mints.usdc }) - const data = `${type}:${id}:${blocknumber}:${purchaserUserId}` + const data = `${type}:${id}:${blocknumber}:${purchaserUserId}:${purchaseAccess}` const memoInstruction = new TransactionInstruction({ keys: [ @@ -609,7 +609,8 @@ export class SolanaWeb3Manager { extraAmount = 0, splits, purchaserUserId, - senderAccount + senderAccount, + purchaseAccess }: { id: number type: 'track' @@ -618,6 +619,7 @@ export class SolanaWeb3Manager { blocknumber: number purchaserUserId: number senderAccount: PublicKey + purchaseAccess: PurchaseAccess }) { if (!this.web3Manager) { throw new Error( @@ -691,7 +693,7 @@ export class SolanaWeb3Manager { this.paymentRouterProgramId ) - const data = `${type}:${id}:${blocknumber}:${purchaserUserId}` + const data = `${type}:${id}:${blocknumber}:${purchaserUserId}:${purchaseAccess}` const memoInstruction = new TransactionInstruction({ keys: [ @@ -721,7 +723,8 @@ export class SolanaWeb3Manager { splits, purchaserUserId, senderKeypair, - skipSendAndReturnTransaction + skipSendAndReturnTransaction, + purchaseAccess }: { id: number type: 'track' @@ -731,6 +734,7 @@ export class SolanaWeb3Manager { purchaserUserId: number senderKeypair: Keypair skipSendAndReturnTransaction?: boolean + purchaseAccess: PurchaseAccess }) { const instructions = await this.getPurchaseContentWithPaymentRouterInstructions({ @@ -740,7 +744,8 @@ export class SolanaWeb3Manager { extraAmount, splits, purchaserUserId, - senderAccount: senderKeypair.publicKey + senderAccount: senderKeypair.publicKey, + purchaseAccess }) const recentBlockhash = (await this.connection.getLatestBlockhash()) .blockhash