Skip to content

Commit

Permalink
Add Token-2022 support (#17)
Browse files Browse the repository at this point in the history
* Add Token-2022 program

* Add asserts for Token-2022

* Make token program account optional

* Update asserts for Token-2022

* Add Token-2022 types

* Support multiple token programs

* wip: update tests to support multiple token programs

* Update generated clients

* Refactor unpack helper

* Fix tests for Token-2022

* Fix tests for Token-2022

* Update edition helper

* Add Token-2022 support to burn instruction

* Use transfer checked

* Remove spl-token dependency

* Update mpl-utils dependency

* Update generated code

* Update generated clients

* Add missing option

* Add missing spl token program

* Update spl-token program resolver condition

* Mint extensions validation

* Add creation and validation for spl-token-2022 mints

* Add metadata pointer error

* Update spl-token-2022 version

* Regenerate clients

* Update tests

* Update generated code

* Move validation to a sparate module

* Add mint and token accounts validation

* Add token program test

* Add spl-token-2022 tests

* Update generated code

* Add token account validation on transfer

* Restrict token program

* Fix warnings

* Validate token owner

* Validate account owner in legacy instructions

* Update clients

* Fix error type

* Check close authority on destination account

* Check immutable owner extension on token account

* Refactor collection check

* Update kinobi version

* Add missing token extension

* Add custom error type

* Add token account tests

* Add ATA non-transferable test

* Remove redundant check

* Add close authority check

* Fix close authority check

* Switch error type

* Validate token close authority on print

* Fix clippy

* Add Token-2022 JS test

* Add owner assert

* Revert to optional token owner (#70)

* Fix optional token owner

* Fix account type

* Improve token program test

* Fix typos

* Remove large slot offset

* Not applicable comments

---------

Co-authored-by: febo <febo@users.noreply.github.com>
  • Loading branch information
febo and febo authored Dec 21, 2023
1 parent 29b8915 commit 4e5bcca
Show file tree
Hide file tree
Showing 111 changed files with 6,328 additions and 2,060 deletions.
68 changes: 68 additions & 0 deletions clients/js/src/generated/errors/mplTokenMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2855,6 +2855,74 @@ nameToErrorMap.set(
CannotChangeUpdateAuthorityWithDelegateError
);

/** InvalidMintExtensionType: Invalid mint extension type */
export class InvalidMintExtensionTypeError extends ProgramError {
override readonly name: string = 'InvalidMintExtensionType';

readonly code: number = 0xc2; // 194

constructor(program: Program, cause?: Error) {
super('Invalid mint extension type', program, cause);
}
}
codeToErrorMap.set(0xc2, InvalidMintExtensionTypeError);
nameToErrorMap.set('InvalidMintExtensionType', InvalidMintExtensionTypeError);

/** InvalidMintCloseAuthority: Invalid mint close authority */
export class InvalidMintCloseAuthorityError extends ProgramError {
override readonly name: string = 'InvalidMintCloseAuthority';

readonly code: number = 0xc3; // 195

constructor(program: Program, cause?: Error) {
super('Invalid mint close authority', program, cause);
}
}
codeToErrorMap.set(0xc3, InvalidMintCloseAuthorityError);
nameToErrorMap.set('InvalidMintCloseAuthority', InvalidMintCloseAuthorityError);

/** InvalidMetadataPointer: Invalid metadata pointer */
export class InvalidMetadataPointerError extends ProgramError {
override readonly name: string = 'InvalidMetadataPointer';

readonly code: number = 0xc4; // 196

constructor(program: Program, cause?: Error) {
super('Invalid metadata pointer', program, cause);
}
}
codeToErrorMap.set(0xc4, InvalidMetadataPointerError);
nameToErrorMap.set('InvalidMetadataPointer', InvalidMetadataPointerError);

/** InvalidTokenExtensionType: Invalid token extension type */
export class InvalidTokenExtensionTypeError extends ProgramError {
override readonly name: string = 'InvalidTokenExtensionType';

readonly code: number = 0xc5; // 197

constructor(program: Program, cause?: Error) {
super('Invalid token extension type', program, cause);
}
}
codeToErrorMap.set(0xc5, InvalidTokenExtensionTypeError);
nameToErrorMap.set('InvalidTokenExtensionType', InvalidTokenExtensionTypeError);

/** MissingImmutableOwnerExtension: Missing immutable owner extension */
export class MissingImmutableOwnerExtensionError extends ProgramError {
override readonly name: string = 'MissingImmutableOwnerExtension';

readonly code: number = 0xc6; // 198

constructor(program: Program, cause?: Error) {
super('Missing immutable owner extension', program, cause);
}
}
codeToErrorMap.set(0xc6, MissingImmutableOwnerExtensionError);
nameToErrorMap.set(
'MissingImmutableOwnerExtension',
MissingImmutableOwnerExtensionError
);

/**
* Attempts to resolve a custom program error from the provided error code.
* @category Errors
Expand Down
21 changes: 16 additions & 5 deletions clients/js/src/generated/instructions/createV1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
resolveCreators,
resolveDecimals,
resolveIsNonFungible,
resolveIsNonFungibleOrIsMintSigner,
resolvePrintSupply,
} from '../../hooked';
import { findMasterEditionPda, findMetadataPda } from '../accounts';
Expand Down Expand Up @@ -297,11 +298,21 @@ export function createV1(
);
}
if (!resolvedAccounts.splTokenProgram.value) {
resolvedAccounts.splTokenProgram.value = context.programs.getPublicKey(
'splToken',
'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'
);
resolvedAccounts.splTokenProgram.isWritable = false;
if (
resolveIsNonFungibleOrIsMintSigner(
context,
resolvedAccounts,
resolvedArgs,
programId,
false
)
) {
resolvedAccounts.splTokenProgram.value = context.programs.getPublicKey(
'splToken',
'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'
);
resolvedAccounts.splTokenProgram.isWritable = false;
}
}
if (!resolvedArgs.isCollection) {
resolvedArgs.isCollection = false;
Expand Down
10 changes: 10 additions & 0 deletions clients/js/src/hooked/resolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
ACCOUNT_HEADER_SIZE,
Context,
Option,
isSigner,
none,
some,
} from '@metaplex-foundation/umi';
Expand Down Expand Up @@ -90,3 +91,12 @@ export const resolveOptionalTokenOwner = (
accounts.token.value
? { value: null }
: { value: context.identity.publicKey };

export const resolveIsNonFungibleOrIsMintSigner = (
context: any,
accounts: ResolvedAccountsWithIndices,
args: { tokenStandard?: TokenStandard },
...rest: any[]
): boolean =>
isNonFungible(expectSome(args.tokenStandard)) ||
isSigner(expectSome(accounts.mint.value));
8 changes: 7 additions & 1 deletion clients/js/test/_setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import {
generateSigner,
percentAmount,
publicKey,
PublicKey,
Signer,
transactionBuilder,
Expand Down Expand Up @@ -50,6 +51,10 @@ export const FUNGIBLE_TOKEN_STANDARDS: TokenStandardKeys[] = [
'Fungible',
];

export const SPL_TOKEN_2022_PROGRAM_ID: PublicKey = publicKey(
'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb'
);

export const createUmi = async () =>
(await baseCreateUmi()).use(mplTokenMetadata());

Expand Down Expand Up @@ -92,7 +97,7 @@ export const createDigitalAssetWithToken = async (
authority: input.authority,
mint: mint.publicKey,
token: input.token,
tokenOwner: input.tokenOwner,
tokenOwner: input.tokenOwner ?? umi.identity.publicKey,
amount: input.amount ?? 1,
tokenStandard: input.tokenStandard ?? TokenStandard.NonFungible,
})
Expand Down Expand Up @@ -127,6 +132,7 @@ export const createDigitalAssetWithVerifiedCreators = async (
mintV1(umi, {
authority: input.authority,
mint: mint.publicKey,
tokenOwner: umi.identity.publicKey,
amount: 1,
tokenStandard: TokenStandard.NonFungible,
})
Expand Down
2 changes: 2 additions & 0 deletions clients/js/test/createHelpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ test('it can create a new NonFungible', async (t) => {
// When we create a new NonFungible at this address.
await createNft(umi, {
mint,
tokenOwner: umi.identity.publicKey,
name: 'My NFT',
uri: 'https://example.com/my-nft.json',
sellerFeeBasisPoints: percentAmount(5.5),
Expand Down Expand Up @@ -86,6 +87,7 @@ test('it can create a new ProgrammableNonFungible', async (t) => {
// When we create a new ProgrammableNonFungible at this address.
await createProgrammableNft(umi, {
mint,
tokenOwner: umi.identity.publicKey,
name: 'My Programmable NFT',
uri: 'https://example.com/my-programmable-nft.json',
sellerFeeBasisPoints: percentAmount(5.5),
Expand Down
67 changes: 66 additions & 1 deletion clients/js/test/createV1.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import {
programmableConfig,
TokenStandard,
} from '../src';
import { createUmi } from './_setup';
import { createUmi, SPL_TOKEN_2022_PROGRAM_ID } from './_setup';

test('it can create a new NonFungible', async (t) => {
// Given a new mint Signer.
Expand Down Expand Up @@ -351,3 +351,68 @@ test('an explicit payer can be used for storage fees', async (t) => {
const payerBalance = await umi.rpc.getBalance(payer.publicKey);
t.deepEqual(payerBalance, subtractAmounts(sol(10), totalFees));
});

test('it can create a new ProgrammableNonFungible with Token-2022', async (t) => {
// Given a new mint Signer.
const umi = await createUmi();
const mint = generateSigner(umi);

// When we create a new ProgrammableNonFungible at this address.
await createV1(umi, {
mint,
name: 'My Programmable NFT',
uri: 'https://example.com/my-programmable-nft.json',
sellerFeeBasisPoints: percentAmount(5.5),
splTokenProgram: SPL_TOKEN_2022_PROGRAM_ID,
tokenStandard: TokenStandard.ProgrammableNonFungible,
}).sendAndConfirm(umi);

// Then a Mint account was created with zero supply.
const mintAccount = await fetchMint(umi, mint.publicKey);
const masterEdition = findMasterEditionPda(umi, { mint: mint.publicKey });
t.like(mintAccount, <Mint>{
publicKey: publicKey(mint),
supply: 0n,
decimals: 0,
mintAuthority: some(publicKey(masterEdition)),
freezeAuthority: some(publicKey(masterEdition)),
});

// And a Metadata account was created.
const metadata = findMetadataPda(umi, { mint: mint.publicKey });
const metadataAccount = await fetchMetadata(umi, metadata);
t.like(metadataAccount, <Metadata>{
publicKey: publicKey(metadata),
updateAuthority: publicKey(umi.identity),
mint: publicKey(mint),
tokenStandard: some(TokenStandard.ProgrammableNonFungible),
name: 'My Programmable NFT',
uri: 'https://example.com/my-programmable-nft.json',
sellerFeeBasisPoints: 550,
primarySaleHappened: false,
isMutable: true,
creators: some([
{ address: publicKey(umi.identity), verified: true, share: 100 },
]),
collection: none(),
uses: none(),
collectionDetails: none(),
programmableConfig: some(programmableConfig('V1', { ruleSet: none() })),
});

// And a MasterEdition account was created.
const masterEditionAccount = await fetchMasterEdition(umi, masterEdition);
t.like(masterEditionAccount, <MasterEdition>{
publicKey: publicKey(masterEdition),
supply: 0n,
maxSupply: some(0n),
});

// And the SPL Token-2022 Program is the owner of the mint account.
const account = await umi.rpc.getAccount(mint.publicKey);
t.true(account.exists);

if (account.exists) {
t.is(account.owner, SPL_TOKEN_2022_PROGRAM_ID);
}
});
1 change: 1 addition & 0 deletions clients/js/test/digitalAssetWithToken.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ test('it can fetch all DigitalAssetWithToken by owner and mint', async (t) => {
mintV1(umi, {
mint: mintA1.publicKey,
token: regularToken.publicKey,
tokenOwner: ownerA,
tokenStandard: TokenStandard.FungibleAsset,
amount: 15,
})
Expand Down
6 changes: 6 additions & 0 deletions clients/js/test/mintV1.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ test('it can mint only one token after a NonFungible is created', async (t) => {
// When we mint one token.
await mintV1(umi, {
mint: mint.publicKey,
tokenOwner: umi.identity.publicKey,
amount: 1,
tokenStandard: TokenStandard.NonFungible,
}).sendAndConfirm(umi);
Expand All @@ -45,6 +46,7 @@ test('it can mint only one token after a NonFungible is created', async (t) => {
// But when we try to mint another token.
const promise = mintV1(umi, {
mint: mint.publicKey,
tokenOwner: umi.identity.publicKey,
amount: 1,
tokenStandard: TokenStandard.NonFungible,
}).sendAndConfirm(umi);
Expand All @@ -68,6 +70,7 @@ test('it can mint only one token after a ProgrammableNonFungible is created', as
// When we mint one token.
await mintV1(umi, {
mint: mint.publicKey,
tokenOwner: umi.identity.publicKey,
amount: 1,
tokenStandard: TokenStandard.ProgrammableNonFungible,
}).sendAndConfirm(umi);
Expand All @@ -85,6 +88,7 @@ test('it can mint only one token after a ProgrammableNonFungible is created', as
// But when we try to mint another token.
const promise = mintV1(umi, {
mint: mint.publicKey,
tokenOwner: umi.identity.publicKey,
amount: 1,
tokenStandard: TokenStandard.ProgrammableNonFungible,
}).sendAndConfirm(umi);
Expand All @@ -108,6 +112,7 @@ test('it can mint multiple tokens after a Fungible is created', async (t) => {
// When we mint 42 token.
await mintV1(umi, {
mint: mint.publicKey,
tokenOwner: umi.identity.publicKey,
amount: 42,
tokenStandard: TokenStandard.Fungible,
}).sendAndConfirm(umi);
Expand Down Expand Up @@ -138,6 +143,7 @@ test('it can mint multiple tokens after a FungibleAsset is created', async (t) =
// When we mint 42 token.
await mintV1(umi, {
mint: mint.publicKey,
tokenOwner: umi.identity.publicKey,
amount: 42,
tokenStandard: TokenStandard.FungibleAsset,
}).sendAndConfirm(umi);
Expand Down
Loading

0 comments on commit 4e5bcca

Please sign in to comment.