diff --git a/clients/js/src/generated/instructions/setData.ts b/clients/js/src/generated/instructions/setData.ts index 45b4fc3..f07a5f7 100644 --- a/clients/js/src/generated/instructions/setData.ts +++ b/clients/js/src/generated/instructions/setData.ts @@ -16,6 +16,7 @@ import { getStructEncoder, getU8Decoder, getU8Encoder, + none, transformEncoder, type Address, type Codec, @@ -107,7 +108,7 @@ export type SetDataInstructionDataArgs = { compression: CompressionArgs; format: FormatArgs; dataSource: DataSourceArgs; - data: OptionOrNullable; + data?: OptionOrNullable; }; export function getSetDataInstructionDataEncoder(): Encoder { @@ -120,7 +121,11 @@ export function getSetDataInstructionDataEncoder(): Encoder ({ ...value, discriminator: SET_DATA_DISCRIMINATOR }) + (value) => ({ + ...value, + discriminator: SET_DATA_DISCRIMINATOR, + data: value.data ?? none(), + }) ); } @@ -166,7 +171,7 @@ export type SetDataInput< compression: SetDataInstructionDataArgs['compression']; format: SetDataInstructionDataArgs['format']; dataSource: SetDataInstructionDataArgs['dataSource']; - data: SetDataInstructionDataArgs['data']; + data?: SetDataInstructionDataArgs['data']; }; export function getSetDataInstruction< diff --git a/clients/js/test/extend.test.ts b/clients/js/test/extend.test.ts new file mode 100644 index 0000000..1639b74 --- /dev/null +++ b/clients/js/test/extend.test.ts @@ -0,0 +1,13 @@ +import test from 'ava'; + +test.todo( + 'the program authority of a canonical metadata account can extend it' +); + +test.todo( + 'the explicit authority of a canonical metadata account can extend it' +); + +test.todo( + 'the metadata authority of a non-canonical metadata account can extend it' +); diff --git a/clients/js/test/setData.test.ts b/clients/js/test/setData.test.ts index 5d3b538..db72a2d 100644 --- a/clients/js/test/setData.test.ts +++ b/clients/js/test/setData.test.ts @@ -1,9 +1,13 @@ +import { getTransferSolInstruction } from '@solana-program/system'; import { address, + appendTransactionMessageInstruction, appendTransactionMessageInstructions, generateKeyPairSigner, getUtf8Encoder, + isSolanaError, pipe, + SOLANA_ERROR__INSTRUCTION_ERROR__INVALID_ACCOUNT_DATA, } from '@solana/web3.js'; import test from 'ava'; import { @@ -14,6 +18,7 @@ import { Format, getSetAuthorityInstruction, getSetDataInstruction, + getSetImmutableInstruction, Metadata, } from '../src'; import { @@ -21,11 +26,11 @@ import { createDefaultSolanaClient, createDefaultTransaction, createDeployedProgram, + createKeypairBuffer, createNonCanonicalMetadata, generateKeyPairSignerWithSol, signAndSendTransaction, } from './_setup'; -import { getTransferSolInstruction } from '@solana-program/system'; test('the program authority of a canonical metadata account can update its data using instruction data', async (t) => { // Given the following authority and deployed program. @@ -224,22 +229,286 @@ test('the authority of a non-canonical metadata account can update its data usin }); }); -test.todo( - 'the program authority of a canonical metadata account can update its data using a pre-allocated buffer' -); +test('the program authority of a canonical metadata account can update its data using a pre-allocated buffer', async (t) => { + // Given the following authority and deployed program. + const client = createDefaultSolanaClient(); + const authority = await generateKeyPairSignerWithSol(client); + const [program, programData] = await createDeployedProgram(client, authority); -test.todo( - 'the explicit authority of a canonical metadata account can update its data using a pre-allocated buffer' -); + // And the following initialized canonical metadata account. + const originalData = getUtf8Encoder().encode('Original data'); + const [metadata] = await createCanonicalMetadata(client, { + authority, + program, + programData, + seed: 'dummy', + encoding: Encoding.None, + compression: Compression.None, + format: Format.None, + dataSource: DataSource.Direct, + data: originalData, + }); -test.todo( - 'the authority of a non-canonical metadata account can update its data using a pre-allocated buffer' -); + // And the following pre-allocated buffer account with written data. + const newData = getUtf8Encoder().encode('https://example.com/new-data.json'); + const buffer = await createKeypairBuffer(client, { + payer: authority, + data: newData, + }); + + // When the program authority updates the data of the metadata account using the buffer. + const setDataIx = getSetDataInstruction({ + metadata, + authority, + buffer: buffer.address, + program, + programData, + encoding: Encoding.Utf8, + compression: Compression.Gzip, + format: Format.Json, + dataSource: DataSource.Url, + }); + await pipe( + await createDefaultTransaction(client, authority), + (tx) => appendTransactionMessageInstruction(setDataIx, tx), + (tx) => signAndSendTransaction(client, tx) + ); + + // Then we expect the metadata account have the new data. + const account = await fetchMetadata(client.rpc, metadata); + t.like(account.data, { + encoding: Encoding.Utf8, + compression: Compression.Gzip, + format: Format.Json, + dataSource: DataSource.Url, + data: newData, + }); +}); + +test('the explicit authority of a canonical metadata account can update its data using a pre-allocated buffer', async (t) => { + // Given the following authorities and deployed program. + const client = createDefaultSolanaClient(); + const [authority, explicitAuthority] = await Promise.all([ + generateKeyPairSignerWithSol(client), + generateKeyPairSigner(), + ]); + const [program, programData] = await createDeployedProgram(client, authority); + + // And the following initialized canonical metadata account. + const originalData = getUtf8Encoder().encode('Original data'); + const [metadata] = await createCanonicalMetadata(client, { + authority, + program, + programData, + seed: 'dummy', + encoding: Encoding.None, + compression: Compression.None, + format: Format.None, + dataSource: DataSource.Direct, + data: originalData, + }); + + // And the following pre-allocated buffer account with written data. + const newData = getUtf8Encoder().encode('https://example.com/new-data.json'); + const buffer = await createKeypairBuffer(client, { + payer: authority, + data: newData, + }); + + // And given an explicit authority is set on the metadata account. + const setAuthorityIx = getSetAuthorityInstruction({ + metadata, + authority, + program, + programData, + newAuthority: explicitAuthority.address, + }); + + // When the explicit authority updates the data of the metadata account using the buffer. + const setDataIx = getSetDataInstruction({ + metadata, + authority: explicitAuthority, + buffer: buffer.address, + program, + programData, + encoding: Encoding.Utf8, + compression: Compression.Gzip, + format: Format.Json, + dataSource: DataSource.Url, + }); + await pipe( + await createDefaultTransaction(client, authority), + (tx) => + appendTransactionMessageInstructions([setAuthorityIx, setDataIx], tx), + (tx) => signAndSendTransaction(client, tx) + ); + + // Then we expect the metadata account have the new data. + const account = await fetchMetadata(client.rpc, metadata); + t.like(account.data, { + encoding: Encoding.Utf8, + compression: Compression.Gzip, + format: Format.Json, + dataSource: DataSource.Url, + data: newData, + }); +}); + +test('the authority of a non-canonical metadata account can update its data using a pre-allocated buffer', async (t) => { + // Given the following authority and deployed program. + const client = createDefaultSolanaClient(); + const authority = await generateKeyPairSignerWithSol(client); + const program = address('TokenKEGQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'); + + // And the following initialized non-canonical metadata account. + const originalData = getUtf8Encoder().encode('Original data'); + const [metadata] = await createNonCanonicalMetadata(client, { + authority, + program, + seed: 'dummy', + encoding: Encoding.None, + compression: Compression.None, + format: Format.None, + dataSource: DataSource.Direct, + data: originalData, + }); + + // And the following pre-allocated buffer account with written data. + const newData = getUtf8Encoder().encode('https://example.com/new-data.json'); + const buffer = await createKeypairBuffer(client, { + payer: authority, + data: newData, + }); + + // When the metadata authority updates the account using the pre-allocated buffer. + const setDataIx = getSetDataInstruction({ + metadata, + authority, + buffer: buffer.address, + program, + encoding: Encoding.Utf8, + compression: Compression.Gzip, + format: Format.Json, + dataSource: DataSource.Url, + }); + await pipe( + await createDefaultTransaction(client, authority), + (tx) => appendTransactionMessageInstruction(setDataIx, tx), + (tx) => signAndSendTransaction(client, tx) + ); + + // Then we expect the metadata account have the new data. + const account = await fetchMetadata(client.rpc, metadata); + t.like(account.data, { + encoding: Encoding.Utf8, + compression: Compression.Gzip, + format: Format.Json, + dataSource: DataSource.Url, + data: newData, + }); +}); + +test('an immutable canonical metadata account cannot be updated', async (t) => { + // Given the following authority and deployed program. + const client = createDefaultSolanaClient(); + const authority = await generateKeyPairSignerWithSol(client); + const [program, programData] = await createDeployedProgram(client, authority); + + // And the following initialized canonical metadata account. + const [metadata] = await createCanonicalMetadata(client, { + authority, + program, + programData, + seed: 'dummy', + data: getUtf8Encoder().encode('Original data'), + }); + + // And given the metadata account is immutable. + const setImmutableIx = getSetImmutableInstruction({ + metadata, + authority, + program, + programData, + }); + + // When the program authority tries to update the data of the metadata account. + const setDataIx = getSetDataInstruction({ + metadata, + authority, + program, + programData, + encoding: Encoding.Utf8, + compression: Compression.None, + format: Format.Json, + dataSource: DataSource.Direct, + data: getUtf8Encoder().encode('New data'), + }); + const promise = pipe( + await createDefaultTransaction(client, authority), + (tx) => + appendTransactionMessageInstructions([setImmutableIx, setDataIx], tx), + (tx) => signAndSendTransaction(client, tx) + ); + + // Then we expect the transaction to fail. + const error = await t.throwsAsync(promise); + t.true( + isSolanaError( + error.cause, + SOLANA_ERROR__INSTRUCTION_ERROR__INVALID_ACCOUNT_DATA + ) + ); +}); + +test('an immutable non-canonical metadata account cannot be updated', async (t) => { + // Given the following authority and deployed program. + const client = createDefaultSolanaClient(); + const authority = await generateKeyPairSignerWithSol(client); + const program = address('TokenKEGQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'); + + // And the following initialized non-canonical metadata account. + const [metadata] = await createNonCanonicalMetadata(client, { + authority, + program, + seed: 'dummy', + data: getUtf8Encoder().encode('Original data'), + }); + + // And given the metadata account is immutable. + const setImmutableIx = getSetImmutableInstruction({ + metadata, + authority, + program, + }); + + // When the metadata authority tries to update the data. + const setDataIx = getSetDataInstruction({ + metadata, + authority, + program, + encoding: Encoding.Utf8, + compression: Compression.None, + format: Format.Json, + dataSource: DataSource.Direct, + data: getUtf8Encoder().encode('New data'), + }); + const promise = pipe( + await createDefaultTransaction(client, authority), + (tx) => + appendTransactionMessageInstructions([setImmutableIx, setDataIx], tx), + (tx) => signAndSendTransaction(client, tx) + ); + + // Then we expect the transaction to fail. + const error = await t.throwsAsync(promise); + t.true( + isSolanaError( + error.cause, + SOLANA_ERROR__INSTRUCTION_ERROR__INVALID_ACCOUNT_DATA + ) + ); +}); test.todo( 'The metadata account needs to be extended for data changes that add more than 1KB' ); - -test.todo('an immutable canonical metadata account cannot be updated'); - -test.todo('an immutable non-canonical metadata account cannot be updated'); diff --git a/clients/js/test/trim.test.ts b/clients/js/test/trim.test.ts new file mode 100644 index 0000000..fcab5a1 --- /dev/null +++ b/clients/js/test/trim.test.ts @@ -0,0 +1,9 @@ +import test from 'ava'; + +test.todo('the program authority of a canonical metadata account can trim it'); + +test.todo('the explicit authority of a canonical metadata account can trim it'); + +test.todo( + 'the metadata authority of a non-canonical metadata account can trim it' +); diff --git a/program/idl.json b/program/idl.json index 5684658..24a46f5 100644 --- a/program/idl.json +++ b/program/idl.json @@ -621,6 +621,7 @@ { "kind": "instructionArgumentNode", "name": "data", + "defaultValue": { "kind": "noneValueNode" }, "docs": [], "type": { "kind": "remainderOptionTypeNode",