From d300b3cef27fa67194a20898698e1c10115ac018 Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Thu, 24 Sep 2026 10:52:05 -0700 Subject: [PATCH 1/2] fix(solana-relay): require close authority on claimable token account creation Only relay Claimable Tokens user bank creations when the same transaction sets the user bank's close authority to the program's rent destination via a SetAuthority instruction signed by the user bank's owner. Reject any other SetAuthority and the program's Close instruction. Requires @audius/spl with Claimable Tokens SetAuthority support. Co-Authored-By: Claude Opus 5.5 --- .../assertRelayAllowedInstructions.test.ts | 301 ++++++++++++++---- .../relay/assertRelayAllowedInstructions.ts | 125 +++++++- 2 files changed, 353 insertions(+), 73 deletions(-) diff --git a/apps/solana-relay/src/routes/relay/assertRelayAllowedInstructions.test.ts b/apps/solana-relay/src/routes/relay/assertRelayAllowedInstructions.test.ts index 37551c9..53895d1 100644 --- a/apps/solana-relay/src/routes/relay/assertRelayAllowedInstructions.test.ts +++ b/apps/solana-relay/src/routes/relay/assertRelayAllowedInstructions.test.ts @@ -5,7 +5,9 @@ import { RewardManagerProgram, RewardManagerInstruction } from '@audius/spl' +import { secp256k1 } from '@noble/curves/secp256k1' import { + AuthorityType, NATIVE_MINT, TOKEN_PROGRAM_ID, createApproveInstruction, @@ -715,22 +717,222 @@ describe('Solana Relay', function () { }) describe('Claimable Tokens Program', function () { + const ethPrivateKey = secp256k1.utils.randomPrivateKey() + const blockhash = getRandomPublicKey().toBase58() + + // @audius/spl uses its own @solana/spl-token, whose AuthorityType enum + // has the same values but is a distinct type + type SplAuthorityType = Parameters< + typeof ClaimableTokensProgram.createSignedSetAuthorityData + >[0]['authorityType'] + + const toEthAddress = (privateKey: Uint8Array) => + '0x' + + Buffer.from( + Secp256k1Program.publicKeyToEthAddress( + secp256k1.getPublicKey(privateKey, false).slice(1) + ) + ).toString('hex') + + /** + * Creates a user bank creation with the Secp256k1 and SetAuthority + * instructions that set its close authority, overridable to build + * invalid variants. + */ + const createUserBankInstructions = async ({ + authority = audioClaimableTokenAuthority, + privateKey = ethPrivateKey, + signerPrivateKey = privateKey, + instructionIndex = 0, + authorityType = AuthorityType.CloseAccount, + newAuthority = ClaimableTokensProgram.rentDestination, + signedUserBank + }: { + authority?: PublicKey + privateKey?: Uint8Array + signerPrivateKey?: Uint8Array + instructionIndex?: number + authorityType?: AuthorityType + newAuthority?: PublicKey + signedUserBank?: PublicKey + } = {}) => { + const ethAddress = toEthAddress(privateKey) + const userBank = await ClaimableTokensProgram.deriveUserBank({ + ethAddress, + claimableTokensPDA: authority + }) + const message = ClaimableTokensProgram.createSignedSetAuthorityData({ + blockhash, + userBank: signedUserBank ?? userBank, + authorityType: authorityType as number as SplAuthorityType, + newAuthority + }) + const create = ClaimableTokensProgram.createAccountInstruction({ + ethAddress, + payer: getRandomPublicKey(), + mint: getRandomPublicKey(), + authority, + userBank, + programId: CLAIMABLE_TOKEN_PROGRAM_ID + }) + const secp = Secp256k1Program.createInstructionWithPrivateKey({ + privateKey: signerPrivateKey, + message, + instructionIndex: instructionIndex + 1 + }) + const setAuthority = ClaimableTokensProgram.createSetAuthorityInstruction({ + userBank, + authority, + programId: CLAIMABLE_TOKEN_PROGRAM_ID + }) + return { + ethAddress, + userBank, + create, + secp, + setAuthority, + instructions: [create, secp, setAuthority] + } + } + + it('should allow creation that sets the close authority to the rent destination', async function () { + const { instructions } = await createUserBankInstructions() + await assertRelayAllowedInstructions(instructions) + }) + + it('should reject creation without setting the close authority', async function () { + const { create } = await createUserBankInstructions() + await expect(assertRelayAllowedInstructions([create])).rejects.toThrow( + 'must set the close authority' + ) + }) + + it('should reject creation that sets the close authority to another account', async function () { + const { instructions } = await createUserBankInstructions({ + newAuthority: getRandomPublicKey() + }) + await expect(assertRelayAllowedInstructions(instructions)).rejects.toThrow( + InvalidRelayInstructionError + ) + }) + + it('should reject creation that transfers ownership instead', async function () { + const { instructions } = await createUserBankInstructions({ + authorityType: AuthorityType.AccountOwner, + newAuthority: getRandomPublicKey() + }) + await expect(assertRelayAllowedInstructions(instructions)).rejects.toThrow( + InvalidRelayInstructionError + ) + }) + + it('should reject a close authority signed by a different wallet', async function () { + const { instructions } = await createUserBankInstructions({ + signerPrivateKey: secp256k1.utils.randomPrivateKey() + }) + await expect(assertRelayAllowedInstructions(instructions)).rejects.toThrow( + InvalidRelayInstructionError + ) + }) + + it('should reject a close authority signed for a different user bank', async function () { + const { instructions } = await createUserBankInstructions({ + signedUserBank: getRandomPublicKey() + }) + await expect(assertRelayAllowedInstructions(instructions)).rejects.toThrow( + InvalidRelayInstructionError + ) + }) + + it('should reject a close authority for a different user bank than the one created', async function () { + const first = await createUserBankInstructions() + const second = await createUserBankInstructions({ + privateKey: secp256k1.utils.randomPrivateKey(), + instructionIndex: 1 + }) + // Creates the first user bank, but sets the close authority of the second + await expect( + assertRelayAllowedInstructions([ + first.create, + second.secp, + second.setAuthority + ]) + ).rejects.toThrow('must set the close authority') + }) + + it('should reject a Secp256k1 instruction with the wrong instruction index', async function () { + const { instructions } = await createUserBankInstructions({ + instructionIndex: 1 + }) + await expect(assertRelayAllowedInstructions(instructions)).rejects.toThrow( + InvalidRelayInstructionError + ) + }) + + it('should reject SetAuthority without a preceding Secp256k1 instruction', async function () { + const { create, setAuthority } = await createUserBankInstructions() + await expect( + assertRelayAllowedInstructions([create, setAuthority]) + ).rejects.toThrow(InvalidRelayInstructionError) + }) + + it('should reject the mainnet ownership transfer SetAuthority', async function () { + // Transaction: 5P5QZjQhzhik7b4YGVmkBpiTRXzKKMqApL86bEjYXMjq8LJkvQkRJzbCyy5yTw7oJjqgcUQoExawXLMUpANuNyts + const userBank = new PublicKey( + 'Bwde2Eu9FQMuV9RTwNj2vng8u92aRV1rjXzMyQJu83Ph' + ) + await expect( + assertRelayAllowedInstructions([ + new TransactionInstruction({ + programId: Secp256k1Program.programId, + keys: [], + data: Buffer.from( + 'ASAAAAwAAGEAZwAAqa3Sldm1AP3Y5pW+XrSKsPWTSCY10DPjmy9H41ybWn1qnOD228tHJKP+2HVAoPYR/cp2NzDisog8Dhz+WpNrj1uvhx0FxK6CFpFnb9JZDCvQvdznAZ5oFp2hVwxgLEHrv0cngiL0NOpUdI/Qgk9bYH/htvWJIwAAAAYCAWNLDNbw11p9JYiGsQAm7ZmCOYs7zB0BRLQWG/CxkJOkopOo9nn6aEg4D4369kn7ivc9gOWiruJ81acljr1S/yQ=', + 'base64' + ) + }), + ClaimableTokensProgram.createSetAuthorityInstruction({ + userBank, + authority: audioClaimableTokenAuthority, + programId: CLAIMABLE_TOKEN_PROGRAM_ID + }) + ]) + ).rejects.toThrow(InvalidRelayInstructionError) + }) + + it('should reject Close instructions', async function () { + const { ethAddress, userBank } = await createUserBankInstructions() + await expect( + assertRelayAllowedInstructions([ + new TransactionInstruction({ + programId: CLAIMABLE_TOKEN_PROGRAM_ID, + keys: [ + { pubkey: userBank, isSigner: false, isWritable: true }, + { + pubkey: audioClaimableTokenAuthority, + isSigner: false, + isWritable: false + }, + { + pubkey: ClaimableTokensProgram.rentDestination, + isSigner: false, + isWritable: true + }, + { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false } + ], + data: Buffer.concat([ + Buffer.from([3]), + Buffer.from(ethAddress.slice(2), 'hex') + ]) + }) + ]) + ).rejects.toThrow('Unsupported Claimable Tokens Program instruction') + }) + it('should not consume the recreation limit for first-time account creation', async function () { - const wallet = '0xe42b199d864489387bf64262874fc6472bcbc151' - const payer = getRandomPublicKey() - const mint = getRandomPublicKey() - const userBank = getRandomPublicKey() + const { userBank, instructions } = await createUserBankInstructions() - await assertRelayAllowedInstructions([ - ClaimableTokensProgram.createAccountInstruction({ - ethAddress: wallet, - payer, - mint, - authority: audioClaimableTokenAuthority, - userBank, - programId: CLAIMABLE_TOKEN_PROGRAM_ID - }) - ]) + await assertRelayAllowedInstructions(instructions) expect(wasClaimableTokenAccountPreviouslyCreated).toHaveBeenCalledWith( userBank.toBase58() @@ -742,21 +944,9 @@ describe('Solana Relay', function () { vi.mocked(wasClaimableTokenAccountPreviouslyCreated).mockResolvedValue( true ) - const wallet = '0xe42b199d864489387bf64262874fc6472bcbc151' - const payer = getRandomPublicKey() - const mint = getRandomPublicKey() - const userBank = getRandomPublicKey() + const { userBank, instructions } = await createUserBankInstructions() - await assertRelayAllowedInstructions([ - ClaimableTokensProgram.createAccountInstruction({ - ethAddress: wallet, - payer, - mint, - authority: audioClaimableTokenAuthority, - userBank, - programId: CLAIMABLE_TOKEN_PROGRAM_ID - }) - ]) + await assertRelayAllowedInstructions(instructions) expect(rateLimitClaimableTokenAccountRecreation).toHaveBeenCalledWith( userBank.toBase58() @@ -772,65 +962,40 @@ describe('Solana Relay', function () { 'System has recreated too many claimable token accounts today' ) ) - const wallet = '0xe42b199d864489387bf64262874fc6472bcbc151' - const payer = getRandomPublicKey() - const mint = getRandomPublicKey() - const userBank = getRandomPublicKey() + const { instructions } = await createUserBankInstructions() - await expect( - assertRelayAllowedInstructions([ - ClaimableTokensProgram.createAccountInstruction({ - ethAddress: wallet, - payer, - mint, - authority: audioClaimableTokenAuthority, - userBank, - programId: CLAIMABLE_TOKEN_PROGRAM_ID - }) - ]) - ).rejects.toThrow( + await expect(assertRelayAllowedInstructions(instructions)).rejects.toThrow( 'System has recreated too many claimable token accounts today' ) }) it('should allow claimable token program instructions with valid authority', async function () { - // Dummy eth address to make the encoder happy - const wallet = '0xe42b199d864489387bf64262874fc6472bcbc151' const payer = getRandomPublicKey() - const mint = getRandomPublicKey() - const userBank = getRandomPublicKey() const destination = getRandomPublicKey() const nonceAccount = getRandomPublicKey() + const usdc = await createUserBankInstructions({ + authority: usdcClaimableTokenAuthority + }) + const audio = await createUserBankInstructions({ + authority: audioClaimableTokenAuthority, + instructionIndex: 4 + }) const instructions = [ - ClaimableTokensProgram.createAccountInstruction({ - ethAddress: wallet, - payer, - mint, - authority: usdcClaimableTokenAuthority, - userBank, - programId: CLAIMABLE_TOKEN_PROGRAM_ID - }), + ...usdc.instructions, ClaimableTokensProgram.createTransferInstruction({ payer, - sourceEthAddress: wallet, - sourceUserBank: userBank, + sourceEthAddress: usdc.ethAddress, + sourceUserBank: usdc.userBank, destination, nonceAccount, authority: usdcClaimableTokenAuthority, programId: CLAIMABLE_TOKEN_PROGRAM_ID }), - ClaimableTokensProgram.createAccountInstruction({ - ethAddress: wallet, - payer, - mint, - authority: audioClaimableTokenAuthority, - userBank, - programId: CLAIMABLE_TOKEN_PROGRAM_ID - }), + ...audio.instructions, ClaimableTokensProgram.createTransferInstruction({ payer, - sourceEthAddress: wallet, - sourceUserBank: userBank, + sourceEthAddress: audio.ethAddress, + sourceUserBank: audio.userBank, destination, nonceAccount, authority: audioClaimableTokenAuthority, diff --git a/apps/solana-relay/src/routes/relay/assertRelayAllowedInstructions.ts b/apps/solana-relay/src/routes/relay/assertRelayAllowedInstructions.ts index ab54efa..7079e38 100644 --- a/apps/solana-relay/src/routes/relay/assertRelayAllowedInstructions.ts +++ b/apps/solana-relay/src/routes/relay/assertRelayAllowedInstructions.ts @@ -11,6 +11,7 @@ import { import { Users } from '@pedalboard/storage' import { ASSOCIATED_TOKEN_PROGRAM_ID, + AuthorityType, NATIVE_MINT, TOKEN_PROGRAM_ID, decodeInstruction, @@ -385,16 +386,97 @@ const assertAllowedRewardsManagerProgramInstruction = ( } } +// Offsets the Claimable Tokens program requires of its Secp256k1 instructions +const CLAIMABLE_TOKENS_SECP_ETH_ADDRESS_OFFSET = 12 +const CLAIMABLE_TOKENS_SECP_SIGNATURE_OFFSET = 32 +const CLAIMABLE_TOKENS_SECP_MESSAGE_DATA_OFFSET = 97 + +/** + * Returns the user bank if the instruction at the given index is a Claimable + * Tokens SetAuthority instruction that sets the user bank's close authority + * to the program's rent destination, authorized by the user bank's owner in + * the Secp256k1 instruction immediately preceding it. Otherwise returns null. + */ +const getCloseAuthorityUserBank = async ( + instructions: TransactionInstruction[], + instructionIndex: number +) => { + const instruction = instructions[instructionIndex] + const secpIndex = instructionIndex - 1 + const secpInstruction = instructions[secpIndex] + if ( + !instruction || + !secpInstruction || + instruction.programId.toBase58() !== CLAIMABLE_TOKEN_PROGRAM_ID || + !secpInstruction.programId.equals(Secp256k1Program.programId) + ) { + return null + } + try { + const decoded = ClaimableTokensProgram.decodeInstruction(instruction) + if (!ClaimableTokensProgram.isSetAuthorityInstruction(decoded)) { + return null + } + const secp = Secp256k1Program.decode(secpInstruction) + if ( + secp.numSignatures !== 1 || + secp.signatureInstructionIndex !== secpIndex || + secp.ethAddressInstructionIndex !== secpIndex || + secp.messageInstructionIndex !== secpIndex || + secp.ethAddressOffset !== CLAIMABLE_TOKENS_SECP_ETH_ADDRESS_OFFSET || + secp.signatureOffset !== CLAIMABLE_TOKENS_SECP_SIGNATURE_OFFSET || + secp.messageDataOffset !== CLAIMABLE_TOKENS_SECP_MESSAGE_DATA_OFFSET + ) { + return null + } + const signed = ClaimableTokensProgram.decodeSignedSetAuthorityData( + secp.message + ) + const userBank = decoded.keys.userBank.pubkey + const signerUserBank = await ClaimableTokensProgram.deriveUserBank({ + ethAddress: '0x' + Buffer.from(secp.ethAddress).toString('hex'), + claimableTokensPDA: decoded.keys.authority.pubkey + }) + if ( + signed.authorityType !== AuthorityType.CloseAccount || + !signed.newAuthority?.equals(ClaimableTokensProgram.rentDestination) || + !signed.userBank.equals(userBank) || + !signerUserBank.equals(userBank) + ) { + return null + } + return userBank + } catch { + return null + } +} + /** * Checks that the claimable token program instruction uses one of the - * allowed mints by checking the authority. + * allowed mints by checking the authority, and protects against feePayer + * rent drain by only allowing user bank creations that set the user bank's + * close authority to the program's rent destination. */ const assertAllowedClaimableTokenProgramInstruction = async ( instructionIndex: number, - instruction: TransactionInstruction + instruction: TransactionInstruction, + instructions: TransactionInstruction[] ) => { - const decodedInstruction = - ClaimableTokensProgram.decodeInstruction(instruction) + let decodedInstruction + try { + decodedInstruction = ClaimableTokensProgram.decodeInstruction(instruction) + } catch { + throw new InvalidRelayInstructionError( + instructionIndex, + 'Unsupported Claimable Tokens Program instruction' + ) + } + if (ClaimableTokensProgram.isCloseInstruction(decodedInstruction)) { + throw new InvalidRelayInstructionError( + instructionIndex, + 'Unsupported Claimable Tokens Program instruction' + ) + } const authority = decodedInstruction.keys.authority.pubkey const allowedMints = await getAllowedMints() const authorities = allowedMints.map((mint) => @@ -410,7 +492,36 @@ const assertAllowedClaimableTokenProgramInstruction = async ( ) } + if (ClaimableTokensProgram.isSetAuthorityInstruction(decodedInstruction)) { + if (!(await getCloseAuthorityUserBank(instructions, instructionIndex))) { + throw new InvalidRelayInstructionError( + instructionIndex, + `Claimable Tokens SetAuthority must set the close authority to ${ClaimableTokensProgram.rentDestination.toBase58()}` + ) + } + } + if (ClaimableTokensProgram.isCreateAccountInstruction(decodedInstruction)) { + let hasCloseAuthority = false + for (let i = instructionIndex + 1; i < instructions.length; i++) { + const closeAuthorityUserBank = await getCloseAuthorityUserBank( + instructions, + i + ) + if ( + closeAuthorityUserBank?.equals(decodedInstruction.keys.userBank.pubkey) + ) { + hasCloseAuthority = true + break + } + } + if (!hasCloseAuthority) { + throw new InvalidRelayInstructionError( + instructionIndex, + `Claimable token account creation must set the close authority to ${ClaimableTokensProgram.rentDestination.toBase58()}` + ) + } + const userBank = decodedInstruction.keys.userBank.pubkey.toBase58() const wasPreviouslyCreated = await wasClaimableTokenAccountPreviouslyCreated(userBank) @@ -702,7 +813,11 @@ export const assertRelayAllowedInstructions = async ( assertAllowedRewardsManagerProgramInstruction(i, instruction) break case CLAIMABLE_TOKEN_PROGRAM_ID: - await assertAllowedClaimableTokenProgramInstruction(i, instruction) + await assertAllowedClaimableTokenProgramInstruction( + i, + instruction, + instructions + ) break case JUPITER_AGGREGATOR_V6_PROGRAM_ID: await assertAllowedJupiterProgramInstruction( From 79ce85315e07e9aba54854a6c38aed9b7cbee480 Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Thu, 24 Sep 2026 12:10:14 -0700 Subject: [PATCH 2/2] chore(solana-relay): bump @audius/spl to 2.2.0 Picks up the Claimable Tokens SetAuthority support required by the close authority check. Co-Authored-By: Claude Opus 5.5 --- apps/solana-relay/package.json | 2 +- package-lock.json | 200 ++++++++++++++++++++++++++++++++- 2 files changed, 199 insertions(+), 3 deletions(-) diff --git a/apps/solana-relay/package.json b/apps/solana-relay/package.json index 50314fa..b0a1fe4 100644 --- a/apps/solana-relay/package.json +++ b/apps/solana-relay/package.json @@ -12,7 +12,7 @@ "preset": "jest-presets/jest/node" }, "dependencies": { - "@audius/spl": "2.1.0", + "@audius/spl": "2.2.0", "@jup-ag/api": "6.0.48", "@metaplex-foundation/umi": "1.4.1", "@metaplex-foundation/umi-bundle-defaults": "1.4.1", diff --git a/package-lock.json b/package-lock.json index b873316..6f2926e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3232,7 +3232,7 @@ "name": "@pedalboard/solana-relay", "version": "0.0.23", "dependencies": { - "@audius/spl": "2.1.0", + "@audius/spl": "2.2.0", "@jup-ag/api": "6.0.48", "@metaplex-foundation/umi": "1.4.1", "@metaplex-foundation/umi-bundle-defaults": "1.4.1", @@ -3291,6 +3291,117 @@ "vitest": "0.34.6" } }, + "apps/solana-relay/node_modules/@audius/spl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@audius/spl/-/spl-2.2.0.tgz", + "integrity": "sha512-JsepnrZKYQPK8CruiF5kFC67Ud3rqR2rFeZPdxxOMUKIQeC8Ka+gQRNoQqFCUIFS67nNBHsq+LFOMhjQ6kqF6g==", + "license": "Apache-2.0", + "dependencies": { + "@coral-xyz/anchor": "0.29.0", + "@noble/hashes": "1.4.0", + "@solana/buffer-layout": "4.0.1", + "@solana/buffer-layout-utils": "0.2.0", + "@solana/spl-token": "0.3.8", + "bs58": "6.0.0" + }, + "peerDependencies": { + "@solana/web3.js": "^1.95.8" + } + }, + "apps/solana-relay/node_modules/@audius/spl/node_modules/@coral-xyz/anchor": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@coral-xyz/anchor/-/anchor-0.29.0.tgz", + "integrity": "sha512-eny6QNG0WOwqV0zQ7cs/b1tIuzZGmP7U7EcH+ogt4Gdbl8HDmIYVMh/9aTmYZPaFWjtUaI8qSn73uYEXWfATdA==", + "license": "(MIT OR Apache-2.0)", + "dependencies": { + "@coral-xyz/borsh": "^0.29.0", + "@noble/hashes": "^1.3.1", + "@solana/web3.js": "^1.68.0", + "bn.js": "^5.1.2", + "bs58": "^4.0.1", + "buffer-layout": "^1.2.2", + "camelcase": "^6.3.0", + "cross-fetch": "^3.1.5", + "crypto-hash": "^1.3.0", + "eventemitter3": "^4.0.7", + "pako": "^2.0.3", + "snake-case": "^3.0.4", + "superstruct": "^0.15.4", + "toml": "^3.0.0" + }, + "engines": { + "node": ">=11" + } + }, + "apps/solana-relay/node_modules/@audius/spl/node_modules/@coral-xyz/anchor/node_modules/@coral-xyz/borsh": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@coral-xyz/borsh/-/borsh-0.29.0.tgz", + "integrity": "sha512-s7VFVa3a0oqpkuRloWVPdCK7hMbAMY270geZOGfCnaqexrP5dTIpbEHL33req6IYPPJ0hYa71cdvJ1h6V55/oQ==", + "license": "Apache-2.0", + "dependencies": { + "bn.js": "^5.1.2", + "buffer-layout": "^1.2.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@solana/web3.js": "^1.68.0" + } + }, + "apps/solana-relay/node_modules/@audius/spl/node_modules/@coral-xyz/anchor/node_modules/bs58": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", + "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", + "license": "MIT", + "dependencies": { + "base-x": "^3.0.2" + } + }, + "apps/solana-relay/node_modules/@audius/spl/node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "apps/solana-relay/node_modules/@audius/spl/node_modules/@solana/spl-token": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@solana/spl-token/-/spl-token-0.3.8.tgz", + "integrity": "sha512-ogwGDcunP9Lkj+9CODOWMiVJEdRtqHAtX2rWF62KxnnSWtMZtV9rDhTrZFshiyJmxDnRL/1nKE1yJHg4jjs3gg==", + "license": "Apache-2.0", + "dependencies": { + "@solana/buffer-layout": "^4.0.0", + "@solana/buffer-layout-utils": "^0.2.0", + "buffer": "^6.0.3" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "@solana/web3.js": "^1.47.4" + } + }, + "apps/solana-relay/node_modules/@audius/spl/node_modules/bs58": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz", + "integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==", + "license": "MIT", + "dependencies": { + "base-x": "^5.0.0" + } + }, + "apps/solana-relay/node_modules/@audius/spl/node_modules/bs58/node_modules/base-x": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.1.tgz", + "integrity": "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==", + "license": "MIT" + }, "apps/solana-relay/node_modules/@coral-xyz/anchor": { "version": "0.31.1", "resolved": "https://registry.npmjs.org/@coral-xyz/anchor/-/anchor-0.31.1.tgz", @@ -49499,7 +49610,7 @@ "version": "file:apps/solana-relay", "requires": { "@audius/sdk": "15.1.0", - "@audius/spl": "2.1.0", + "@audius/spl": "2.2.0", "@jup-ag/api": "6.0.48", "@metaplex-foundation/umi": "1.4.1", "@metaplex-foundation/umi-bundle-defaults": "1.4.1", @@ -49555,6 +49666,91 @@ "web3-utils": "4.2.3" }, "dependencies": { + "@audius/spl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@audius/spl/-/spl-2.2.0.tgz", + "integrity": "sha512-JsepnrZKYQPK8CruiF5kFC67Ud3rqR2rFeZPdxxOMUKIQeC8Ka+gQRNoQqFCUIFS67nNBHsq+LFOMhjQ6kqF6g==", + "requires": { + "@coral-xyz/anchor": "0.29.0", + "@noble/hashes": "1.4.0", + "@solana/buffer-layout": "4.0.1", + "@solana/buffer-layout-utils": "0.2.0", + "@solana/spl-token": "0.3.8", + "bs58": "6.0.0" + }, + "dependencies": { + "@coral-xyz/anchor": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@coral-xyz/anchor/-/anchor-0.29.0.tgz", + "integrity": "sha512-eny6QNG0WOwqV0zQ7cs/b1tIuzZGmP7U7EcH+ogt4Gdbl8HDmIYVMh/9aTmYZPaFWjtUaI8qSn73uYEXWfATdA==", + "requires": { + "@coral-xyz/borsh": "^0.29.0", + "@noble/hashes": "^1.3.1", + "@solana/web3.js": "^1.68.0", + "bn.js": "^5.1.2", + "bs58": "^4.0.1", + "buffer-layout": "^1.2.2", + "camelcase": "^6.3.0", + "cross-fetch": "^3.1.5", + "crypto-hash": "^1.3.0", + "eventemitter3": "^4.0.7", + "pako": "^2.0.3", + "snake-case": "^3.0.4", + "superstruct": "^0.15.4", + "toml": "^3.0.0" + }, + "dependencies": { + "@coral-xyz/borsh": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@coral-xyz/borsh/-/borsh-0.29.0.tgz", + "integrity": "sha512-s7VFVa3a0oqpkuRloWVPdCK7hMbAMY270geZOGfCnaqexrP5dTIpbEHL33req6IYPPJ0hYa71cdvJ1h6V55/oQ==", + "requires": { + "bn.js": "^5.1.2", + "buffer-layout": "^1.2.0" + } + }, + "bs58": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", + "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", + "requires": { + "base-x": "^3.0.2" + } + } + } + }, + "@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==" + }, + "@solana/spl-token": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@solana/spl-token/-/spl-token-0.3.8.tgz", + "integrity": "sha512-ogwGDcunP9Lkj+9CODOWMiVJEdRtqHAtX2rWF62KxnnSWtMZtV9rDhTrZFshiyJmxDnRL/1nKE1yJHg4jjs3gg==", + "requires": { + "@solana/buffer-layout": "^4.0.0", + "@solana/buffer-layout-utils": "^0.2.0", + "buffer": "^6.0.3" + } + }, + "bs58": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz", + "integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==", + "requires": { + "base-x": "^5.0.0" + }, + "dependencies": { + "base-x": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.1.tgz", + "integrity": "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==" + } + } + } + } + }, "@coral-xyz/anchor": { "version": "0.31.1", "resolved": "https://registry.npmjs.org/@coral-xyz/anchor/-/anchor-0.31.1.tgz",