From b3a739b6a377b86e9c2ef34177601b0afc766479 Mon Sep 17 00:00:00 2001 From: facing-n Date: Fri, 29 Sep 2023 13:45:15 -0400 Subject: [PATCH 1/2] add payer for release init functions (#1127) * add deletegated payer for release init functions * program v 0.2.16 --- Cargo.lock | 2 +- programs/nina/Cargo.toml | 2 +- programs/nina/src/errors.rs | 2 ++ .../nina/src/instructions/hub_add_release.rs | 2 +- .../nina/src/instructions/release_init.rs | 24 +++++++++---------- .../src/instructions/release_init_via_hub.rs | 23 +++++++++++++----- .../instructions/release_init_with_credit.rs | 23 +++++++++--------- programs/nina/src/state/hub.rs | 4 ++-- programs/nina/src/state/release.rs | 2 +- programs/nina/src/utils.rs | 5 ++++ tests/index.js | 8 +++++-- 11 files changed, 58 insertions(+), 39 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a509fe208..f22718414 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1211,7 +1211,7 @@ dependencies = [ [[package]] name = "nina" -version = "0.2.14" +version = "0.2.15" dependencies = [ "anchor-lang", "anchor-spl", diff --git a/programs/nina/Cargo.toml b/programs/nina/Cargo.toml index 6e57c1a35..35c252e28 100644 --- a/programs/nina/Cargo.toml +++ b/programs/nina/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nina" -version = "0.2.15" +version = "0.2.16" description = "Nina - A self-publishing protocol" edition = "2018" diff --git a/programs/nina/src/errors.rs b/programs/nina/src/errors.rs index 05da3d200..770581050 100644 --- a/programs/nina/src/errors.rs +++ b/programs/nina/src/errors.rs @@ -76,4 +76,6 @@ pub enum ErrorCode { SubscriptionPayerMismatch, #[msg("Subscription Delegated Payer Mismatch")] SubscriptionDelegatedPayerMismatch, + #[msg("Release Init Delegated Payer Mismatch")] + ReleaseInitDelegatedPayerMismatch } \ No newline at end of file diff --git a/programs/nina/src/instructions/hub_add_release.rs b/programs/nina/src/instructions/hub_add_release.rs index 8d2f123c8..ab7d73001 100644 --- a/programs/nina/src/instructions/hub_add_release.rs +++ b/programs/nina/src/instructions/hub_add_release.rs @@ -54,7 +54,7 @@ pub fn handler ( &mut ctx.accounts.hub_content, &mut ctx.accounts.hub_release, ctx.accounts.release.clone(), - ctx.accounts.authority.clone(), + ctx.accounts.authority.key(), false, if ctx.remaining_accounts.len() == 1 {Some(ctx.remaining_accounts[0].clone())} else {None} )?; diff --git a/programs/nina/src/instructions/release_init.rs b/programs/nina/src/instructions/release_init.rs index 28eb0e091..5335c922d 100644 --- a/programs/nina/src/instructions/release_init.rs +++ b/programs/nina/src/instructions/release_init.rs @@ -1,6 +1,7 @@ use anchor_lang::prelude::*; -use anchor_spl::token::{self, TokenAccount, Mint, Token, Burn}; - +use anchor_spl::token::{self, TokenAccount, Mint, Token}; +use crate::utils::{file_service_account}; +use crate::errors::ErrorCode; use crate::state::*; #[derive(Accounts)] @@ -22,16 +23,8 @@ pub struct ReleaseInitialize<'info> { pub release_mint: Box>, #[account(mut)] pub payer: Signer<'info>, - /// CHECK: originally we would allow delegated payment for a release - /// anyone could publish a release on behalf of someone else - /// but we now require the authority == payer - /// in order to prevent someone from publishing a release on behalf of someone else - /// without their express approval. - /// Originally desired behavior will require more checks for approval - #[account( - mut, - constraint = payer.key() == authority.key(), - )] + /// CHECK: This is safe because we check in the handler that authority === payer + /// or that payer is nina operated file-service wallet pub authority: UncheckedAccount<'info>, #[account( constraint = authority_token_account.owner == authority.key(), @@ -62,6 +55,11 @@ pub fn handler( bumps: ReleaseBumps, metadata_data: ReleaseMetadataData, ) -> Result<()> { + if ctx.accounts.payer.key() != ctx.accounts.authority.key() { + if ctx.accounts.payer.key() != file_service_account::ID { + return Err(ErrorCode::ReleaseInitDelegatedPayerMismatch.into()); + } + } Release::release_init_handler( &ctx.accounts.release, @@ -81,7 +79,7 @@ pub fn handler( ctx.accounts.release_signer.to_account_info().clone(), ctx.accounts.metadata.to_account_info().clone(), ctx.accounts.release_mint.clone(), - ctx.accounts.payer.clone(), + ctx.accounts.authority.clone(), ctx.accounts.metadata_program.to_account_info().clone(), ctx.accounts.token_program.clone(), ctx.accounts.system_program.clone(), diff --git a/programs/nina/src/instructions/release_init_via_hub.rs b/programs/nina/src/instructions/release_init_via_hub.rs index 1eef064cc..2400c45b3 100644 --- a/programs/nina/src/instructions/release_init_via_hub.rs +++ b/programs/nina/src/instructions/release_init_via_hub.rs @@ -1,5 +1,7 @@ use anchor_lang::prelude::*; use anchor_spl::token::{self, TokenAccount, Mint, Token}; +use crate::utils::{file_service_account}; +use crate::errors::ErrorCode; use crate::state::*; #[derive(Accounts)] @@ -11,12 +13,12 @@ use crate::state::*; )] pub struct ReleaseInitializeViaHub<'info> { #[account(mut)] - pub authority: Signer<'info>, + pub payer: Signer<'info>, #[account( init, seeds = [b"nina-release".as_ref(), release_mint.key().as_ref()], bump, - payer = authority, + payer = payer, space = 1210 )] pub release: AccountLoader<'info, Release>, @@ -42,7 +44,7 @@ pub struct ReleaseInitializeViaHub<'info> { init, seeds = [b"nina-hub-release".as_ref(), hub.key().as_ref(), release.key().as_ref()], bump, - payer = authority, + payer = payer, space = 120 )] pub hub_release: Box>, @@ -50,7 +52,7 @@ pub struct ReleaseInitializeViaHub<'info> { init, seeds = [b"nina-hub-content".as_ref(), hub.key().as_ref(), release.key().as_ref()], bump, - payer = authority, + payer = payer, space = 153 )] pub hub_content: Box>, @@ -87,6 +89,9 @@ pub struct ReleaseInitializeViaHub<'info> { pub metadata_program: AccountInfo<'info>, pub system_program: Program<'info, System>, pub rent: Sysvar<'info, Rent>, + /// CHECK: This is safe because we check in the handler that authority === payer + /// or that payer is nina operated file-service wallet + pub authority: UncheckedAccount<'info>, } pub fn handler( @@ -96,6 +101,12 @@ pub fn handler( metadata_data: ReleaseMetadataData, _hub_handle: String, ) -> Result<()> { + if ctx.accounts.payer.key() != ctx.accounts.authority.key() { + if ctx.accounts.payer.key() != file_service_account::ID { + return Err(ErrorCode::ReleaseInitDelegatedPayerMismatch.into()); + } + } + Hub::hub_collaborator_can_add_or_publish_content( &mut ctx.accounts.hub_collaborator, true @@ -106,7 +117,7 @@ pub fn handler( ctx.accounts.release_signer.to_account_info().clone(), ctx.accounts.release_mint.to_account_info().clone(), ctx.accounts.payment_mint.to_account_info().clone(), - ctx.accounts.authority.to_account_info().clone(), + ctx.accounts.payer.to_account_info().clone(), ctx.accounts.authority.to_account_info().clone(), ctx.accounts.authority_token_account.to_account_info().clone(), ctx.accounts.royalty_token_account.to_account_info(), @@ -147,7 +158,7 @@ pub fn handler( &mut ctx.accounts.hub_content, &mut ctx.accounts.hub_release, ctx.accounts.release.clone(), - ctx.accounts.authority.clone(), + ctx.accounts.authority.key(), true, None, )?; diff --git a/programs/nina/src/instructions/release_init_with_credit.rs b/programs/nina/src/instructions/release_init_with_credit.rs index 8f5399ead..b510083e8 100644 --- a/programs/nina/src/instructions/release_init_with_credit.rs +++ b/programs/nina/src/instructions/release_init_with_credit.rs @@ -2,7 +2,8 @@ use anchor_lang::prelude::*; use anchor_spl::token::{self, TokenAccount, Mint, Token, Burn}; use crate::state::*; -use crate::utils::{nina_publishing_credit_mint}; +use crate::utils::{nina_publishing_credit_mint, file_service_account}; +use crate::errors::ErrorCode; #[derive(Accounts)] pub struct ReleaseInitializeWithCredit<'info> { @@ -23,16 +24,8 @@ pub struct ReleaseInitializeWithCredit<'info> { pub release_mint: Box>, #[account(mut)] pub payer: Signer<'info>, - /// CHECK: originally we would allow delegated payment for a release - /// anyone could publish a release on behalf of someone else - /// but we now require the authority == payer - /// in order to prevent someone from publishing a release on behalf of someone else - /// without their express approval. - /// Originally desired behavior will require more checks for approval - #[account( - mut, - constraint = payer.key() == authority.key(), - )] + /// CHECK: This is safe because we check in the handler that authority === payer + /// or that payer is nina operated file-service wallet pub authority: UncheckedAccount<'info>, #[account( constraint = authority_token_account.owner == authority.key(), @@ -75,6 +68,12 @@ pub fn handler( bumps: ReleaseBumps, metadata_data: ReleaseMetadataData, ) -> Result<()> { + if ctx.accounts.payer.key() != ctx.accounts.authority.key() { + if ctx.accounts.payer.key() != file_service_account::ID { + return Err(ErrorCode::ReleaseInitDelegatedPayerMismatch.into()); + } + } + // Redeemer burn redeemable token let cpi_program = ctx.accounts.token_program.to_account_info().clone(); @@ -105,7 +104,7 @@ pub fn handler( ctx.accounts.release_signer.to_account_info().clone(), ctx.accounts.metadata.to_account_info().clone(), ctx.accounts.release_mint.clone(), - ctx.accounts.payer.clone(), + ctx.accounts.authority.clone(), ctx.accounts.metadata_program.to_account_info().clone(), ctx.accounts.token_program.clone(), ctx.accounts.system_program.clone(), diff --git a/programs/nina/src/state/hub.rs b/programs/nina/src/state/hub.rs index ef824b28f..9823b5baa 100644 --- a/programs/nina/src/state/hub.rs +++ b/programs/nina/src/state/hub.rs @@ -25,11 +25,11 @@ impl Hub { hub_content: &mut Box>, hub_release: &mut Box>, release: AccountLoader<'info, Release>, - authority: Signer<'info>, + authority: Pubkey, is_published_through_hub: bool, reposted_from_hub: Option ) -> Result<()> { - hub_content.added_by = authority.key(); + hub_content.added_by = authority; hub_content.hub = hub.key(); hub_content.child = hub_release.key(); hub_content.content_type = HubContentType::NinaReleaseV1; diff --git a/programs/nina/src/state/release.rs b/programs/nina/src/state/release.rs index b5877b17e..da21d536b 100644 --- a/programs/nina/src/state/release.rs +++ b/programs/nina/src/state/release.rs @@ -240,7 +240,7 @@ impl Release { release_signer: AccountInfo<'info>, metadata: AccountInfo<'info>, release_mint: Box>, - authority: Signer<'info>, + authority: UncheckedAccount<'info>, metadata_program: AccountInfo<'info>, token_program: Program<'info, Token>, system_program: Program<'info, System>, diff --git a/programs/nina/src/utils.rs b/programs/nina/src/utils.rs index c6dab573b..b8794f391 100644 --- a/programs/nina/src/utils.rs +++ b/programs/nina/src/utils.rs @@ -32,3 +32,8 @@ pub mod dispatcher_account { use solana_program::declare_id; declare_id!("BnhxwsrY5aaeMehsTRoJzX2X4w5sKMhMfBs2MCKUqMC"); } + +pub mod file_service_account { + use solana_program::declare_id; + declare_id!("3skAZNf7EjUus6VNNgHog44JZFsp8BBaso9pBRgYntSd"); +} \ No newline at end of file diff --git a/tests/index.js b/tests/index.js index 8662c27f0..7cebe8fb7 100644 --- a/tests/index.js +++ b/tests/index.js @@ -607,8 +607,8 @@ describe('Release', async () => { } ); }, (err) => { - assert.equal(err.error.errorCode.number, 2003); - assert.equal(err.error.errorMessage, "A raw constraint was violated"); + assert.equal(err.error.errorCode.number, 6037); + assert.equal(err.error.errorMessage, "Release Init Delegated Payer Mismatch"); return true; } ); @@ -4430,6 +4430,7 @@ describe('Hub', async () => { hubParams.handle, { accounts: { authority: provider.wallet.publicKey, + payer: provider.wallet.publicKey, release: releaseAccount, releaseSigner: releaseSigner, hub, @@ -4568,6 +4569,7 @@ describe('Hub', async () => { metadataData, hubParams.handle, { accounts: { + payer: user1.publicKey, authority: user1.publicKey, release: releaseAccount, releaseSigner, @@ -4973,6 +4975,7 @@ describe('Hub', async () => { metadataData, hubParams.handle, { accounts: { + payer: user2.publicKey, authority: user2.publicKey, release: releaseAccount, releaseSigner: releaseSigner, @@ -5099,6 +5102,7 @@ describe('Hub', async () => { metadataData, hubParams.handle, { accounts: { + payer: user3.publicKey, authority: user3.publicKey, release: releaseAccount, releaseSigner: releaseSigner, From 085874d49241bf311e41e6bb1780a18b53cc862f Mon Sep 17 00:00:00 2001 From: Eric Farber Date: Tue, 3 Oct 2023 16:50:20 -0400 Subject: [PATCH 2/2] Release modal (#1128) * solBalance check in releasePurchaseHelper * verification-modal * pass message to modal --- js/hubs/components/ReleasePurchase.js | 15 ++++ js/sdk/rollup.config.js | 1 + js/sdk/src/components/UnverifiedModal.js | 84 +++++++++++++++++++++++ js/sdk/src/contexts/Release/release.js | 4 +- js/sdk/src/index.js | 1 + js/sdk/src/utils/releasePurchaseHelper.js | 3 +- js/web/components/ReleasePurchase.js | 15 ++++ 7 files changed, 121 insertions(+), 2 deletions(-) create mode 100644 js/sdk/src/components/UnverifiedModal.js diff --git a/js/hubs/components/ReleasePurchase.js b/js/hubs/components/ReleasePurchase.js index 9169f9dbc..313c67e8f 100644 --- a/js/hubs/components/ReleasePurchase.js +++ b/js/hubs/components/ReleasePurchase.js @@ -28,6 +28,10 @@ const WalletConnectModal = dynamic(() => import('@nina-protocol/nina-internal-sdk/esm/WalletConnectModal') ) +const UnverifiedModal = dynamic(() => + import('@nina-protocol/nina-internal-sdk/esm/UnverifiedModal') +) + import dynamic from 'next/dynamic' const BUTTON_WIDTH = '155px' @@ -67,6 +71,8 @@ const ReleasePurchase = (props) => { const [showNoSolModal, setShowNoSolModal] = useState(false) const [showWalletModal, setShowWalletModal] = useState(false) const [coinflowPurchasePending, setCoinflowPurchasePending] = useState(false) + const [showUnverifiedModal, setShowUnverifiedModal] = useState(false) + const [verificationError, setVerificationError] = useState('') const txPending = useMemo( () => releasePurchaseTransactionPending[releasePubkey], @@ -171,6 +177,10 @@ const ReleasePurchase = (props) => { } const showCompletedTransaction = (result) => { + if (result.msg.indexOf('Unauthorized') > -1) { + setShowUnverifiedModal(true) + setVerificationError(result.msg) + } enqueueSnackbar(result.msg, { variant: result.success ? 'success' : 'warn', }) @@ -331,6 +341,11 @@ const ReleasePurchase = (props) => { + ) } diff --git a/js/sdk/rollup.config.js b/js/sdk/rollup.config.js index 9c8e61a62..8d157fa17 100644 --- a/js/sdk/rollup.config.js +++ b/js/sdk/rollup.config.js @@ -62,6 +62,7 @@ export default [ CoinflowModal: 'src/components/CoinflowModal.js', PurchaseModal: 'src/components/PurchaseModal.js', CoinflowWithdrawModal: 'src/components/CoinflowWithdrawModal.js', + UnverifiedModal: 'src/components/UnverifiedModal.js', }, output: [ { diff --git a/js/sdk/src/components/UnverifiedModal.js b/js/sdk/src/components/UnverifiedModal.js new file mode 100644 index 000000000..bbe9e44ef --- /dev/null +++ b/js/sdk/src/components/UnverifiedModal.js @@ -0,0 +1,84 @@ +import React from 'react' +import { styled } from '@mui/material/styles' +import Paper from '@mui/material/Paper' +import Modal from '@mui/material/Modal' +import Backdrop from '@mui/material/Backdrop' +import Fade from '@mui/material/Fade' +import Button from '@mui/material/Button' +import Typography from '@mui/material/Typography' +import Link from 'next/link' + +const UnverifiedModal = ({ open, setOpen, error }) => { + const handleClose = () => { + setOpen(false) + } + return ( + + handleClose()} + closeAfterTransition + BackdropComponent={Backdrop} + BackdropProps={{ + timeout: 500, + }} + > + + + + {error} + + {' '} + + + + + ) +} + +const Root = styled('div')(() => ({ + display: 'flex', + alignItems: 'center', + width: '100%', +})) + +const StyledModal = styled(Modal)(() => ({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', +})) + +const StyledPaper = styled(Paper)(({ theme }) => ({ + backgroundColor: theme.palette.background.paper, + border: '2px solid #000', + boxShadow: theme.shadows[5], + padding: theme.spacing(2, 4, 3), + width: '40vw', + maxHeight: '90vh', + overflowY: 'auto', + zIndex: '10', + display: 'flex', + flexDirection: 'column', + [theme.breakpoints.down('md')]: { + width: 'unset', + margin: '15px', + padding: theme.spacing(2), + }, +})) + +export default UnverifiedModal diff --git a/js/sdk/src/contexts/Release/release.js b/js/sdk/src/contexts/Release/release.js index 0f785ff30..91beff5a4 100644 --- a/js/sdk/src/contexts/Release/release.js +++ b/js/sdk/src/contexts/Release/release.js @@ -572,6 +572,7 @@ const releaseContextHelper = ({ provider, ninaClient, usdcBalance, + solBalance, hubPubkey ) @@ -887,7 +888,8 @@ const releaseContextHelper = ({ releasePubkey, provider, ninaClient, - usdcBalance + usdcBalance, + solBalance ) await getConfirmTransaction(txid, provider.connection) diff --git a/js/sdk/src/index.js b/js/sdk/src/index.js index cdf9a6262..423f50b50 100644 --- a/js/sdk/src/index.js +++ b/js/sdk/src/index.js @@ -39,3 +39,4 @@ export { default as HubPostCreate } from './components/HubPostCreate' export { default as CoinflowModal } from './components/CoinflowModal' export { default as PurchaseModal } from './components/PurchaseModal' export { default as CoinflowWithdrawModal } from './components/CoinflowWithdrawModal' +export { default as UnverifiedModal } from './components/UnverifiedModal' diff --git a/js/sdk/src/utils/releasePurchaseHelper.js b/js/sdk/src/utils/releasePurchaseHelper.js index b316272a8..a6606520d 100644 --- a/js/sdk/src/utils/releasePurchaseHelper.js +++ b/js/sdk/src/utils/releasePurchaseHelper.js @@ -120,6 +120,7 @@ const releasePurchaseHelper = async ( provider, ninaClient, usdcBalance, + solBalance, hubPubkey = null ) => { let hub @@ -127,7 +128,7 @@ const releasePurchaseHelper = async ( const program = await ninaClient.useProgram() const release = await program.account.release.fetch(releasePubkey) - if (release.price.toNumber() === 0) { + if (release.price.toNumber() === 0 && solBalance === 0) { const message = new TextEncoder().encode(releasePubkey.toBase58()) const messageBase64 = encodeBase64(message) const signature = await provider.wallet.signMessage(message) diff --git a/js/web/components/ReleasePurchase.js b/js/web/components/ReleasePurchase.js index 4bfae0239..87f9d7bca 100644 --- a/js/web/components/ReleasePurchase.js +++ b/js/web/components/ReleasePurchase.js @@ -41,6 +41,10 @@ const WalletConnectModal = dynamic(() => import('@nina-protocol/nina-internal-sdk/esm/WalletConnectModal') ) +const UnverifiedModal = dynamic(() => + import('@nina-protocol/nina-internal-sdk/esm/UnverifiedModal') +) + const ReleasePurchase = (props) => { const { releasePubkey, @@ -85,6 +89,8 @@ const ReleasePurchase = (props) => { const [description, setDescription] = useState() const [showWalletModal, setShowWalletModal] = useState(false) const [coinflowPurchasePending, setCoinflowPurchasePending] = useState(false) + const [showUnverifiedModal, setShowUnverifiedModal] = useState(false) + const [verificationError, setVerificationError] = useState('') const txPending = useMemo( () => releasePurchaseTransactionPending[releasePubkey], @@ -207,6 +213,10 @@ const ReleasePurchase = (props) => { } const showCompletedTransaction = (result) => { + if (result.msg.indexOf('Unauthorized') > -1) { + setShowUnverifiedModal(true) + setVerificationError(result.msg) + } enqueueSnackbar(result.msg, { variant: result.success ? 'success' : 'warn', }) @@ -415,6 +425,11 @@ const ReleasePurchase = (props) => { )} + ) }