Skip to content
Open
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
12 changes: 12 additions & 0 deletions apps/web/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,30 @@ 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';
import { SetHook } from '@/components/instructions/SetHook';
import { BlockTokenExtension } from '@/components/instructions/BlockTokenExtension';
import { SetArbiter } from '@/components/instructions/SetArbiter';
import { RemoveExtension } from '@/components/instructions/RemoveExtension';
import { UnblockTokenExtension } from '@/components/instructions/UnblockTokenExtension';
import { Deposit } from '@/components/instructions/Deposit';
import { Withdraw } from '@/components/instructions/Withdraw';

type InstructionId =
| 'createEscrow'
| 'updateAdmin'
| 'setImmutable'
| 'allowMint'
| 'blockMint'
| 'addTimelock'
| 'setHook'
| 'blockTokenExtension'
| 'unblockTokenExtension'
| 'setArbiter'
| 'removeExtension'
| 'deposit'
| 'withdraw';

Expand All @@ -39,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 All @@ -49,7 +56,9 @@ const NAV: {
{ id: 'addTimelock', label: 'Add Timelock' },
{ id: 'setHook', label: 'Set Hook' },
{ id: 'blockTokenExtension', label: 'Block Token Ext' },
{ id: 'unblockTokenExtension', label: 'Unblock Token Ext' },
{ id: 'setArbiter', label: 'Set Arbiter' },
{ id: 'removeExtension', label: 'Remove Extension' },
],
},
{
Expand All @@ -64,12 +73,15 @@ 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 },
setHook: { title: 'Set Hook', component: SetHook },
blockTokenExtension: { title: 'Block Token Extension', component: BlockTokenExtension },
unblockTokenExtension: { title: 'Unblock Token Extension', component: UnblockTokenExtension },
setArbiter: { title: 'Set Arbiter', component: SetArbiter },
removeExtension: { title: 'Remove Extension', component: RemoveExtension },
deposit: { title: 'Deposit', component: Deposit },
withdraw: { title: 'Withdraw', component: Withdraw },
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const EXTENSION_OPTIONS = [
{ label: 'Pausable (25)', value: '25' },
{ label: 'TransferFeeConfig (1)', value: '1' },
{ label: 'MintCloseAuthority (3)', value: '3' },
{ label: 'MetadataPointer (18)', value: '18' },
];

export function BlockTokenExtension() {
Expand Down
88 changes: 88 additions & 0 deletions apps/web/src/components/instructions/RemoveExtension.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
'use client';

import { useState } from 'react';
import type { Address } from '@solana/kit';
import { getRemoveExtensionInstructionAsync } 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, SelectField, SendButton } from './shared';

const EXTENSION_OPTIONS = [
{ label: 'Timelock (0)', value: '0' },
{ label: 'Hook (1)', value: '1' },
{ label: 'Blocked Token Extensions (2)', value: '2' },
{ label: 'Arbiter (3)', value: '3' },
];

export function RemoveExtension() {
const { createSigner } = useWallet();
const { send, sending, signature, error, reset } = useSendTx();
const { defaultEscrow, rememberEscrow } = useSavedValues();
const { programId } = useProgramContext();
const [escrow, setEscrow] = useState('');
const [extensionType, setExtensionType] = useState('0');
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 = await getRemoveExtensionInstructionAsync(
{
admin: signer,
escrow: escrow as Address,
extensionType: Number(extensionType),
payer: signer,
},
{ programAddress: programId as Address },
);
const txSignature = await send([ix], {
action: 'Remove Extension',
values: { escrow, extensionType },
});
if (txSignature) {
rememberEscrow(escrow);
}
};

return (
<form
onSubmit={e => {
void handleSubmit(e);
}}
style={{ display: 'flex', flexDirection: 'column', gap: 16 }}
>
<FormField
label="Escrow Address"
value={escrow}
onChange={setEscrow}
autoFillValue={defaultEscrow}
onAutoFill={setEscrow}
placeholder="Escrow PDA address"
required
/>
<SelectField
label="Extension Type"
value={extensionType}
onChange={setExtensionType}
options={EXTENSION_OPTIONS}
hint="Select which escrow extension to remove"
/>
<SendButton sending={sending} />
<TxResult signature={signature} error={formError ?? error} />
</form>
);
}
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>
);
}
91 changes: 91 additions & 0 deletions apps/web/src/components/instructions/UnblockTokenExtension.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
'use client';

import { useState } from 'react';
import type { Address } from '@solana/kit';
import { getUnblockTokenExtensionInstructionAsync } 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, SelectField, SendButton } from './shared';

const EXTENSION_OPTIONS = [
{ label: 'NonTransferable (5)', value: '5' },
{ label: 'PermanentDelegate (8)', value: '8' },
{ label: 'TransferHook (16)', value: '16' },
{ label: 'Pausable (25)', value: '25' },
{ label: 'TransferFeeConfig (1)', value: '1' },
{ label: 'MintCloseAuthority (3)', value: '3' },
{ label: 'MetadataPointer (18)', value: '18' },
];

export function UnblockTokenExtension() {
const { createSigner } = useWallet();
const { send, sending, signature, error, reset } = useSendTx();
const { defaultEscrow, rememberEscrow } = useSavedValues();
const { programId } = useProgramContext();
const [escrow, setEscrow] = useState('');
const [blockedExtension, setBlockedExtension] = useState('5');
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 = await getUnblockTokenExtensionInstructionAsync(
{
admin: signer,
escrow: escrow as Address,
blockedExtension: Number(blockedExtension),
payer: signer,
},
{ programAddress: programId as Address },
);
const txSignature = await send([ix], {
action: 'Unblock Token Extension',
values: { escrow, extensionType: blockedExtension },
});
if (txSignature) {
rememberEscrow(escrow);
}
};

return (
<form
onSubmit={e => {
void handleSubmit(e);
}}
style={{ display: 'flex', flexDirection: 'column', gap: 16 }}
>
<FormField
label="Escrow Address"
value={escrow}
onChange={setEscrow}
autoFillValue={defaultEscrow}
onAutoFill={setEscrow}
placeholder="Escrow PDA address"
required
/>
<SelectField
label="Extension Type"
value={blockedExtension}
onChange={setBlockedExtension}
options={EXTENSION_OPTIONS}
hint="Token-2022 extension type to remove from escrow blocked list"
/>
<SendButton sending={sending} />
<TxResult signature={signature} error={formError ?? error} />
</form>
);
}
1 change: 1 addition & 0 deletions apps/web/src/contexts/RecentTransactionsContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface RecentTransactionValues {
mint?: string;
receipt?: string;
amount?: string;
extensionType?: string;
lockDuration?: string;
hookProgram?: string;
rentRecipient?: string;
Expand Down
4 changes: 4 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 All @@ -15,6 +16,7 @@ import {
ESCROW_PROGRAM_ERROR__PERMANENT_DELEGATE_NOT_ALLOWED,
ESCROW_PROGRAM_ERROR__TIMELOCK_NOT_EXPIRED,
ESCROW_PROGRAM_ERROR__TOKEN_EXTENSION_ALREADY_BLOCKED,
ESCROW_PROGRAM_ERROR__TOKEN_EXTENSION_NOT_BLOCKED,
ESCROW_PROGRAM_ERROR__ZERO_DEPOSIT_AMOUNT,
} from '@solana/escrow-program-client';

Expand All @@ -32,8 +34,10 @@ const ESCROW_PROGRAM_ERROR_MESSAGES: Record<number, string> = {
[ESCROW_PROGRAM_ERROR__NON_TRANSFERABLE_NOT_ALLOWED]: 'Mint has NonTransferable extension which is not allowed',
[ESCROW_PROGRAM_ERROR__PAUSABLE_NOT_ALLOWED]: 'Mint has Pausable extension which is not allowed',
[ESCROW_PROGRAM_ERROR__TOKEN_EXTENSION_ALREADY_BLOCKED]: 'Token extension already blocked',
[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
Loading
Loading