Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions apps/web/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { QuickDefaults } from '@/components/QuickDefaults';
import { RecentTransactions } from '@/components/RecentTransactions';
import { CreateEscrow } from '@/components/instructions/CreateEscrow';
import { UpdateAdmin } from '@/components/instructions/UpdateAdmin';
import { SetImmutable } from '@/components/instructions/SetImmutable';
import { AllowMint } from '@/components/instructions/AllowMint';
import { BlockMint } from '@/components/instructions/BlockMint';
import { AddTimelock } from '@/components/instructions/AddTimelock';
Expand All @@ -23,6 +24,7 @@ import { Withdraw } from '@/components/instructions/Withdraw';
type InstructionId =
| 'createEscrow'
| 'updateAdmin'
| 'setImmutable'
| 'allowMint'
| 'blockMint'
| 'addTimelock'
Expand All @@ -43,6 +45,7 @@ const NAV: {
items: [
{ id: 'createEscrow', label: 'Create Escrow' },
{ id: 'updateAdmin', label: 'Update Admin' },
{ id: 'setImmutable', label: 'Set Immutable' },
{ id: 'allowMint', label: 'Allow Mint' },
{ id: 'blockMint', label: 'Block Mint' },
],
Expand Down Expand Up @@ -70,6 +73,7 @@ const NAV: {
const PANELS: Record<InstructionId, { title: string; component: React.ComponentType }> = {
createEscrow: { title: 'Create Escrow', component: CreateEscrow },
updateAdmin: { title: 'Update Admin', component: UpdateAdmin },
setImmutable: { title: 'Set Immutable', component: SetImmutable },
allowMint: { title: 'Allow Mint', component: AllowMint },
blockMint: { title: 'Block Mint', component: BlockMint },
addTimelock: { title: 'Add Timelock', component: AddTimelock },
Expand Down
78 changes: 78 additions & 0 deletions apps/web/src/components/instructions/SetImmutable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
'use client';

import { useState } from 'react';
import type { Address } from '@solana/kit';
import { Badge } from '@solana/design-system/badge';
import { getSetImmutableInstruction } from '@solana/escrow-program-client';
import { useSendTx } from '@/hooks/useSendTx';
import { useSavedValues } from '@/contexts/SavedValuesContext';
import { useWallet } from '@/contexts/WalletContext';
import { useProgramContext } from '@/contexts/ProgramContext';
import { TxResult } from '@/components/TxResult';
import { firstValidationError, validateAddress } from '@/lib/validation';
import { FormField, SendButton } from './shared';

export function SetImmutable() {
const { createSigner } = useWallet();
const { send, sending, signature, error, reset } = useSendTx();
const { defaultEscrow, rememberEscrow } = useSavedValues();
const { programId } = useProgramContext();
const [escrow, setEscrow] = useState('');
const [formError, setFormError] = useState<string | null>(null);

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
reset();
setFormError(null);
const signer = createSigner();
if (!signer) return;

const validationError = firstValidationError(validateAddress(escrow, 'Escrow address'));
if (validationError) {
setFormError(validationError);
return;
}

const ix = getSetImmutableInstruction(
{
admin: signer,
escrow: escrow as Address,
},
{ programAddress: programId as Address },
);

const txSignature = await send([ix], {
action: 'Set Immutable',
values: { escrow },
});
if (txSignature) {
rememberEscrow(escrow);
}
};

return (
<form
onSubmit={e => {
void handleSubmit(e);
}}
style={{ display: 'flex', flexDirection: 'column', gap: 16 }}
>
<div>
<Badge variant="warning">
This action is one-way. Escrow configuration becomes permanently immutable.
</Badge>
</div>
<FormField
label="Escrow Address"
value={escrow}
onChange={setEscrow}
autoFillValue={defaultEscrow}
onAutoFill={setEscrow}
placeholder="Escrowae7..."
required
/>
<SendButton sending={sending} />
<TxResult signature={signature} error={formError ?? error} />
</form>
);
}
2 changes: 2 additions & 0 deletions apps/web/src/lib/transactionErrors.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use client';

import {
ESCROW_PROGRAM_ERROR__ESCROW_IMMUTABLE,
ESCROW_PROGRAM_ERROR__HOOK_PROGRAM_MISMATCH,
ESCROW_PROGRAM_ERROR__HOOK_REJECTED,
ESCROW_PROGRAM_ERROR__INVALID_ADMIN,
Expand Down Expand Up @@ -36,6 +37,7 @@ const ESCROW_PROGRAM_ERROR_MESSAGES: Record<number, string> = {
[ESCROW_PROGRAM_ERROR__TOKEN_EXTENSION_NOT_BLOCKED]: 'Token extension is not currently blocked',
[ESCROW_PROGRAM_ERROR__ZERO_DEPOSIT_AMOUNT]: 'Zero deposit amount',
[ESCROW_PROGRAM_ERROR__INVALID_ARBITER]: 'Arbiter signer is missing or does not match',
[ESCROW_PROGRAM_ERROR__ESCROW_IMMUTABLE]: 'Escrow is immutable and cannot be modified',
};

const FALLBACK_TX_FAILED_MESSAGE = 'Transaction failed';
Expand Down
114 changes: 114 additions & 0 deletions idl/escrow_program.json
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,18 @@
"type": {
"kind": "publicKeyTypeNode"
}
},
{
"kind": "structFieldTypeNode",
"name": "isImmutable",
"type": {
"kind": "booleanTypeNode",
"size": {
"endian": "le",
"format": "u8",
"kind": "numberTypeNode"
}
}
}
],
"kind": "structTypeNode"
Expand Down Expand Up @@ -601,6 +613,29 @@
"kind": "structTypeNode"
}
},
{
"kind": "definedTypeNode",
"name": "setImmutableEvent",
"type": {
"fields": [
{
"kind": "structFieldTypeNode",
"name": "escrow",
"type": {
"kind": "publicKeyTypeNode"
}
},
{
"kind": "structFieldTypeNode",
"name": "admin",
"type": {
"kind": "publicKeyTypeNode"
}
}
],
"kind": "structTypeNode"
}
},
{
"kind": "definedTypeNode",
"name": "withdrawEvent",
Expand Down Expand Up @@ -744,6 +779,12 @@
"kind": "errorNode",
"message": "Token extension is not currently blocked",
"name": "tokenExtensionNotBlocked"
},
{
"code": 16,
"kind": "errorNode",
"message": "Escrow is immutable and cannot be modified",
"name": "escrowImmutable"
}
],
"instructions": [
Expand Down Expand Up @@ -2775,6 +2816,79 @@
],
"kind": "instructionNode",
"name": "unblockTokenExtension"
},
{
"accounts": [
{
"docs": [
"Admin authority for the escrow"
],
"isSigner": true,
"isWritable": false,
"kind": "instructionAccountNode",
"name": "admin"
},
{
"docs": [
"Escrow account to lock as immutable"
],
"isSigner": false,
"isWritable": true,
"kind": "instructionAccountNode",
"name": "escrow"
},
{
"defaultValue": {
"kind": "publicKeyValueNode",
"publicKey": "Eq63FWYo9DXgwoTnpK9gjp7BH4PyhSPo11zEF9FK7f4M"
},
"docs": [
"Event authority PDA for CPI event emission"
],
"isSigner": false,
"isWritable": false,
"kind": "instructionAccountNode",
"name": "eventAuthority"
},
{
"defaultValue": {
"kind": "publicKeyValueNode",
"publicKey": "Escrowae7RaUfNn4oEZHywMXE5zWzYCXenwrCDaEoifg"
},
"docs": [
"Escrow program for CPI event emission"
],
"isSigner": false,
"isWritable": false,
"kind": "instructionAccountNode",
"name": "escrowProgram"
}
],
"arguments": [
{
"defaultValue": {
"kind": "numberValueNode",
"number": 12
},
"defaultValueStrategy": "omitted",
"kind": "instructionArgumentNode",
"name": "discriminator",
"type": {
"endian": "le",
"format": "u8",
"kind": "numberTypeNode"
}
}
],
"discriminators": [
{
"kind": "fieldDiscriminatorNode",
"name": "discriminator",
"offset": 0
}
],
"kind": "instructionNode",
"name": "setImmutable"
}
],
"kind": "programNode",
Expand Down
4 changes: 3 additions & 1 deletion program/src/entrypoint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ use crate::{
instructions::{
process_add_timelock, process_allow_mint, process_block_mint, process_block_token_extension,
process_create_escrow, process_deposit, process_emit_event, process_remove_extension, process_set_arbiter,
process_set_hook, process_unblock_token_extension, process_update_admin, process_withdraw,
process_set_hook, process_set_immutable, process_unblock_token_extension, process_update_admin,
process_withdraw,
},
traits::EscrowInstructionDiscriminators,
};
Expand Down Expand Up @@ -36,6 +37,7 @@ pub fn process_instruction(program_id: &Address, accounts: &[AccountView], instr
process_unblock_token_extension(program_id, accounts, instruction_data)
}
EscrowInstructionDiscriminators::SetArbiter => process_set_arbiter(program_id, accounts, instruction_data),
EscrowInstructionDiscriminators::SetImmutable => process_set_immutable(program_id, accounts, instruction_data),
EscrowInstructionDiscriminators::EmitEvent => process_emit_event(program_id, accounts),
}
}
8 changes: 8 additions & 0 deletions program/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ pub enum EscrowProgramError {
/// (15) Token extension is not currently blocked
#[error("Token extension is not currently blocked")]
TokenExtensionNotBlocked,

/// (16) Escrow is immutable and cannot be modified
#[error("Escrow is immutable and cannot be modified")]
EscrowImmutable,
}

impl From<EscrowProgramError> for ProgramError {
Expand Down Expand Up @@ -99,5 +103,9 @@ mod tests {

let error: ProgramError = EscrowProgramError::InvalidWithdrawer.into();
assert_eq!(error, ProgramError::Custom(5));

let error: ProgramError = EscrowProgramError::EscrowImmutable.into();
assert_eq!(error, ProgramError::Custom(16));
assert_eq!(error, ProgramError::Custom(16));
}
}
2 changes: 2 additions & 0 deletions program/src/events/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pub mod block_mint;
pub mod create_escrow;
pub mod deposit;
pub mod extensions;
pub mod set_immutable;
pub mod shared;
pub mod withdraw;

Expand All @@ -13,5 +14,6 @@ pub use block_mint::*;
pub use create_escrow::*;
pub use deposit::*;
pub use extensions::*;
pub use set_immutable::*;
pub use shared::*;
pub use withdraw::*;
63 changes: 63 additions & 0 deletions program/src/events/set_immutable.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
use alloc::vec::Vec;
use codama::CodamaType;
use pinocchio::Address;

use crate::traits::{EventDiscriminator, EventDiscriminators, EventSerialize};

#[derive(CodamaType)]
pub struct SetImmutableEvent {
pub escrow: Address,
pub admin: Address,
}

impl EventDiscriminator for SetImmutableEvent {
const DISCRIMINATOR: u8 = EventDiscriminators::SetImmutable as u8;
}

impl EventSerialize for SetImmutableEvent {
#[inline(always)]
fn to_bytes_inner(&self) -> Vec<u8> {
let mut data = Vec::with_capacity(Self::DATA_LEN);
data.extend_from_slice(self.escrow.as_ref());
data.extend_from_slice(self.admin.as_ref());
data
}
}

impl SetImmutableEvent {
pub const DATA_LEN: usize = 32 + 32; // escrow + admin

#[inline(always)]
pub fn new(escrow: Address, admin: Address) -> Self {
Self { escrow, admin }
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::events::EVENT_IX_TAG_LE;
use crate::traits::EVENT_DISCRIMINATOR_LEN;

#[test]
fn test_set_immutable_event_new() {
let escrow = Address::new_from_array([1u8; 32]);
let admin = Address::new_from_array([2u8; 32]);
let event = SetImmutableEvent::new(escrow, admin);

assert_eq!(event.escrow, escrow);
assert_eq!(event.admin, admin);
}

#[test]
fn test_set_immutable_event_to_bytes() {
let escrow = Address::new_from_array([1u8; 32]);
let admin = Address::new_from_array([2u8; 32]);
let event = SetImmutableEvent::new(escrow, admin);

let bytes = event.to_bytes();
assert_eq!(bytes.len(), EVENT_DISCRIMINATOR_LEN + SetImmutableEvent::DATA_LEN);
assert_eq!(&bytes[..8], EVENT_IX_TAG_LE);
assert_eq!(bytes[8], EventDiscriminators::SetImmutable as u8);
}
}
1 change: 1 addition & 0 deletions program/src/instructions/allow_mint/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ pub fn process_allow_mint(program_id: &Address, accounts: &[AccountView], instru
let escrow_data = ix.accounts.escrow.try_borrow()?;
let escrow = Escrow::from_account(&escrow_data, ix.accounts.escrow, program_id)?;
escrow.validate_admin(ix.accounts.admin.address())?;
escrow.require_mutable()?;

// Validate AllowedMint PDA using external seeds
let pda_seeds = AllowedMintPda::new(ix.accounts.escrow.address(), ix.accounts.mint.address());
Expand Down
Loading
Loading