Skip to content

Commit

Permalink
[PAY-2337] Purchase Tracks via SDK: sdk.tracks.purchase() (#8278)
Browse files Browse the repository at this point in the history
  • Loading branch information
rickyrombo committed May 8, 2024
1 parent 1cef77a commit 75169cf
Show file tree
Hide file tree
Showing 55 changed files with 1,604 additions and 207 deletions.
6 changes: 6 additions & 0 deletions .changeset/spicy-elephants-shout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@audius/sdk': minor
'@audius/spl': major
---

Update to use PaymentRouterProgram in @audius/spl and enable track purchase in SDK
39 changes: 38 additions & 1 deletion packages/commands/src/purchase-content.mjs
Original file line number Diff line number Diff line change
@@ -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')
Expand Down Expand Up @@ -79,3 +80,39 @@ program

process.exit(0)
})


program.command('purchase-track')
.description('Buys a track using USDC')
.argument('<id>', '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)
})
20 changes: 10 additions & 10 deletions packages/commands/src/route-tokens-to-user-bank.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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)

Expand Down
44 changes: 24 additions & 20 deletions packages/commands/src/utils.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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
}
});
}

Expand Down
4 changes: 2 additions & 2 deletions packages/common/src/services/audius-backend/solana.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -436,7 +436,7 @@ export const purchaseContentWithPaymentRouter = async (
skipSendAndReturnTransaction: true,
purchaseAccess
})
return tx
return tx as Transaction
}

export const findAssociatedTokenAddress = async (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -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'

Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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()}`
Expand Down Expand Up @@ -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:
Expand Down
38 changes: 34 additions & 4 deletions packages/discovery-provider/src/api/v1/models/access_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
{
Expand All @@ -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(
Expand All @@ -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(
Expand Down
28 changes: 21 additions & 7 deletions packages/discovery-provider/src/api/v1/models/tracks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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),
Expand All @@ -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"
),
},
)

Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit 75169cf

Please sign in to comment.