Skip to content

Commit cef819d

Browse files
committed
Add JS test for updating data past the realloc limit
1 parent bcedde9 commit cef819d

2 files changed

Lines changed: 153 additions & 33 deletions

File tree

clients/js/test/_setup.ts

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
import {
66
Address,
77
airdropFactory,
8+
appendTransactionMessageInstruction,
89
appendTransactionMessageInstructions,
910
BASE_ACCOUNT_SIZE,
1011
Commitment,
@@ -26,6 +27,7 @@ import {
2627
sendAndConfirmTransactionFactory,
2728
setTransactionMessageFeePayerSigner,
2829
setTransactionMessageLifetimeUsingBlockhash,
30+
Signature,
2931
signTransactionMessageWithSigners,
3032
SolanaRpcApi,
3133
SolanaRpcSubscriptionsApi,
@@ -41,6 +43,7 @@ import {
4143
findNonCanonicalPda,
4244
Format,
4345
getAllocateInstruction,
46+
getExtendInstruction,
4447
getInitializeInstruction,
4548
getProgramDataPda as getLoaderV3ProgramDataPda,
4649
getWriteInstruction,
@@ -52,6 +55,7 @@ import { getDeployWithMaxDataLenInstruction as getLoaderV3DeployInstruction } fr
5255
import { getInitializeBufferInstruction as getLoaderV3InitializeBufferInstruction } from './loader-v3/initializeBuffer';
5356
import { getWriteInstruction as getLoaderV3WriteInstruction } from './loader-v3/write';
5457

58+
export const REALLOC_LIMIT = 10_240;
5559
const SMALLER_VALID_PROGRAM_BINARY =
5660
'f0VMRgIBAQAAAAAAAAAAAAMA9wABAAAA6AAAAAAAAABAAAAAAAAAAMgBAAAAAAAAAAAAAEAAOAADAEAABgAFAAEAAAAFAAAA6AAAAAAAAADoAAAAAAAAAOgAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAAQAAAAAAAAAQAAAAQAAABgAQAAAAAAAGABAAAAAAAAYAEAAAAAAAA8AAAAAAAAADwAAAAAAAAAABAAAAAAAAACAAAABgAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAHAAAAAAAAAAcAAAAAAAAAAIAAAAAAAAAJUAAAAAAAAAHgAAAAAAAAAEAAAAAAAAAAYAAAAAAAAAYAEAAAAAAAALAAAAAAAAABgAAAAAAAAABQAAAAAAAACQAQAAAAAAAAoAAAAAAAAADAAAAAAAAAAWAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAQAAEA6AAAAAAAAAAAAAAAAAAAAABlbnRyeXBvaW50AAAudGV4dAAuZHluYW1pYwAuZHluc3ltAC5keW5zdHIALnNoc3RydGFiAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAABAAAABgAAAAAAAADoAAAAAAAAAOgAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAHAAAABgAAAAMAAAAAAAAA8AAAAAAAAADwAAAAAAAAAHAAAAAAAAAABAAAAAAAAAAIAAAAAAAAABAAAAAAAAAAEAAAAAsAAAACAAAAAAAAAGABAAAAAAAAYAEAAAAAAAAwAAAAAAAAAAQAAAABAAAACAAAAAAAAAAYAAAAAAAAABgAAAADAAAAAgAAAAAAAACQAQAAAAAAAJABAAAAAAAADAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAgAAAAAwAAAAAAAAAAAAAAAAAAAAAAAACcAQAAAAAAACoAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAA';
5761

@@ -212,9 +216,9 @@ export async function createBuffer(
212216
const { buffer, authority, program, programData, seed, data } = input;
213217
const payer = input.payer ?? authority;
214218
const dataLenth = input.dataLength ?? input.data?.length ?? 0;
215-
const bufferSize = BigInt(96 + dataLenth);
219+
const bufferSize = 96 + dataLenth;
216220
const [rent, defaultTransaction] = await Promise.all([
217-
client.rpc.getMinimumBalanceForRentExemption(bufferSize).send(),
221+
client.rpc.getMinimumBalanceForRentExemption(BigInt(bufferSize)).send(),
218222
createDefaultTransaction(client, payer),
219223
]);
220224
const preFundIx = getTransferSolInstruction({
@@ -230,16 +234,44 @@ export async function createBuffer(
230234
seed,
231235
});
232236
const instructions: IInstruction[] = [preFundIx, allocateIx];
233-
if (data) {
234-
instructions.push(
235-
getWriteInstruction({ buffer, authority, offset: 0, data })
236-
);
237+
if (dataLenth >= REALLOC_LIMIT) {
238+
let offset = 0;
239+
while (offset < dataLenth) {
240+
const length =
241+
dataLenth - offset < REALLOC_LIMIT ? dataLenth - offset : REALLOC_LIMIT;
242+
instructions.push(
243+
getExtendInstruction({ account: buffer, authority, length })
244+
);
245+
offset += length;
246+
}
237247
}
238248
await pipe(
239249
defaultTransaction,
240250
(tx) => appendTransactionMessageInstructions(instructions, tx),
241251
(tx) => signAndSendTransaction(client, tx)
242252
);
253+
if (data) {
254+
let offset = 0;
255+
const chunkSize = 900;
256+
const writePromises: Promise<Signature>[] = [];
257+
while (offset < data.length) {
258+
const writeIx = getWriteInstruction({
259+
buffer,
260+
authority,
261+
offset,
262+
data: data.slice(offset, offset + chunkSize),
263+
});
264+
writePromises.push(
265+
pipe(
266+
defaultTransaction,
267+
(tx) => appendTransactionMessageInstruction(writeIx, tx),
268+
(tx) => signAndSendTransaction(client, tx)
269+
)
270+
);
271+
offset += chunkSize;
272+
}
273+
await Promise.all(writePromises);
274+
}
243275
}
244276

245277
export async function createCanonicalBuffer(

clients/js/test/setData.test.ts

Lines changed: 115 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
isSolanaError,
88
pipe,
99
SOLANA_ERROR__INSTRUCTION_ERROR__INVALID_ACCOUNT_DATA,
10+
SOLANA_ERROR__INSTRUCTION_ERROR__INVALID_REALLOC,
1011
} from '@solana/web3.js';
1112
import test from 'ava';
1213
import {
@@ -15,6 +16,7 @@ import {
1516
Encoding,
1617
fetchMetadata,
1718
Format,
19+
getExtendInstruction,
1820
getSetAuthorityInstruction,
1921
getSetDataInstruction,
2022
getSetImmutableInstruction,
@@ -28,6 +30,8 @@ import {
2830
createKeypairBuffer,
2931
createNonCanonicalMetadata,
3032
generateKeyPairSignerWithSol,
33+
getRentWithoutHeader,
34+
REALLOC_LIMIT,
3135
signAndSendTransaction,
3236
} from './_setup';
3337

@@ -53,10 +57,10 @@ test('the program authority of a canonical metadata account can update its data
5357

5458
// And given we fund the metadata account for the extra space needed for the new data.
5559
const newData = getUtf8Encoder().encode('https://example.com/new-data.json');
56-
const extraSpace = BigInt(newData.length - originalData.length);
57-
const extraRent = await client.rpc
58-
.getMinimumBalanceForRentExemption(extraSpace)
59-
.send();
60+
const extraRent = await getRentWithoutHeader(
61+
client,
62+
newData.length - originalData.length
63+
);
6064
const transferIx = getTransferSolInstruction({
6165
source: authority,
6266
destination: metadata,
@@ -126,10 +130,10 @@ test('the explicit authority of a canonical metadata account can update its data
126130

127131
// And given we fund the metadata account for the extra space needed for the new data.
128132
const newData = getUtf8Encoder().encode('https://example.com/new-data.json');
129-
const extraSpace = BigInt(newData.length - originalData.length);
130-
const extraRent = await client.rpc
131-
.getMinimumBalanceForRentExemption(extraSpace)
132-
.send();
133+
const extraRent = await getRentWithoutHeader(
134+
client,
135+
newData.length - originalData.length
136+
);
133137
const transferIx = getTransferSolInstruction({
134138
source: authority,
135139
destination: metadata,
@@ -190,10 +194,10 @@ test('the authority of a non-canonical metadata account can update its data usin
190194

191195
// And given we fund the metadata account for the extra space needed for the new data.
192196
const newData = getUtf8Encoder().encode('https://example.com/new-data.json');
193-
const extraSpace = BigInt(newData.length - originalData.length);
194-
const extraRent = await client.rpc
195-
.getMinimumBalanceForRentExemption(extraSpace)
196-
.send();
197+
const extraRent = await getRentWithoutHeader(
198+
client,
199+
newData.length - originalData.length
200+
);
197201
const transferIx = getTransferSolInstruction({
198202
source: authority,
199203
destination: metadata,
@@ -256,10 +260,10 @@ test('the program authority of a canonical metadata account can update its data
256260
});
257261

258262
// When the program authority updates the data of the metadata account using the buffer.
259-
const extraSize = BigInt(newData.length - originalData.length);
260-
const extraRent = await client.rpc
261-
.getMinimumBalanceForRentExemption(extraSize)
262-
.send();
263+
const extraRent = await getRentWithoutHeader(
264+
client,
265+
newData.length - originalData.length
266+
);
263267
const fundMetadataIx = getTransferSolInstruction({
264268
source: authority,
265269
destination: metadata,
@@ -334,10 +338,10 @@ test('the explicit authority of a canonical metadata account can update its data
334338
});
335339

336340
// When the explicit authority updates the data of the metadata account using the buffer.
337-
const extraSize = BigInt(newData.length - originalData.length);
338-
const extraRent = await client.rpc
339-
.getMinimumBalanceForRentExemption(extraSize)
340-
.send();
341+
const extraRent = await getRentWithoutHeader(
342+
client,
343+
newData.length - originalData.length
344+
);
341345
const fundMetadataIx = getTransferSolInstruction({
342346
source: authority,
343347
destination: metadata,
@@ -402,10 +406,10 @@ test('the authority of a non-canonical metadata account can update its data usin
402406
});
403407

404408
// When the metadata authority updates the account using the pre-allocated buffer.
405-
const extraSize = BigInt(newData.length - originalData.length);
406-
const extraRent = await client.rpc
407-
.getMinimumBalanceForRentExemption(extraSize)
408-
.send();
409+
const extraRent = await getRentWithoutHeader(
410+
client,
411+
newData.length - originalData.length
412+
);
409413
const fundMetadataIx = getTransferSolInstruction({
410414
source: authority,
411415
destination: metadata,
@@ -540,6 +544,90 @@ test('an immutable non-canonical metadata account cannot be updated', async (t)
540544
);
541545
});
542546

543-
test.todo(
544-
'The metadata account needs to be extended for data changes that add more than 1KB'
545-
);
547+
test('The metadata account needs to be extended for data changes that add more than 1KB', async (t) => {
548+
// Given the following authority and deployed program.
549+
const client = createDefaultSolanaClient();
550+
const authority = await generateKeyPairSignerWithSol(client);
551+
const program = address('TokenKEGQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA');
552+
553+
// And the following initialized metadata account with 200 bytes of data.
554+
const originalData = getUtf8Encoder().encode('x'.repeat(200));
555+
const [metadata] = await createNonCanonicalMetadata(client, {
556+
authority,
557+
program,
558+
seed: 'dummy',
559+
encoding: Encoding.None,
560+
compression: Compression.None,
561+
format: Format.None,
562+
dataSource: DataSource.Direct,
563+
data: originalData,
564+
});
565+
566+
// And the following pre-allocated buffer account with written data.
567+
const newData = getUtf8Encoder().encode(
568+
'x'.repeat(originalData.length + REALLOC_LIMIT + 1)
569+
);
570+
const buffer = await createKeypairBuffer(client, {
571+
payer: authority,
572+
data: newData,
573+
});
574+
575+
// And given the following instructions to fund extra rent, extend extra space and update the data.
576+
const extraSize = newData.length - originalData.length;
577+
const extraRent = await getRentWithoutHeader(client, extraSize);
578+
const transferIx = getTransferSolInstruction({
579+
source: authority,
580+
destination: metadata,
581+
amount: extraRent,
582+
});
583+
const extendIx = getExtendInstruction({
584+
account: metadata,
585+
authority,
586+
length: REALLOC_LIMIT,
587+
});
588+
const setDataIx = getSetDataInstruction({
589+
metadata,
590+
authority,
591+
program,
592+
encoding: Encoding.Utf8,
593+
compression: Compression.Gzip,
594+
format: Format.Json,
595+
dataSource: DataSource.Url,
596+
buffer: buffer.address,
597+
});
598+
599+
// When we try to update the data without extending the account.
600+
const promise = pipe(
601+
await createDefaultTransaction(client, authority),
602+
(tx) => appendTransactionMessageInstructions([transferIx, setDataIx], tx),
603+
(tx) => signAndSendTransaction(client, tx)
604+
);
605+
606+
// Then we expect a program error.
607+
const error = await t.throwsAsync(promise);
608+
t.true(isSolanaError(error));
609+
t.true(
610+
isSolanaError(error.cause, SOLANA_ERROR__INSTRUCTION_ERROR__INVALID_REALLOC)
611+
);
612+
613+
// But when we extend the account and try again.
614+
await pipe(
615+
await createDefaultTransaction(client, authority),
616+
(tx) =>
617+
appendTransactionMessageInstructions(
618+
[transferIx, extendIx, setDataIx],
619+
tx
620+
),
621+
(tx) => signAndSendTransaction(client, tx)
622+
);
623+
624+
// Then we expect the metadata account have the new data.
625+
const account = await fetchMetadata(client.rpc, metadata);
626+
t.like(account.data, <Metadata>{
627+
encoding: Encoding.Utf8,
628+
compression: Compression.Gzip,
629+
format: Format.Json,
630+
dataSource: DataSource.Url,
631+
data: newData,
632+
});
633+
});

0 commit comments

Comments
 (0)