diff --git a/js/stateless.js/src/constants.ts b/js/stateless.js/src/constants.ts index 32a29701ee..b4ca44113a 100644 --- a/js/stateless.js/src/constants.ts +++ b/js/stateless.js/src/constants.ts @@ -1,3 +1,4 @@ +import { findProgramAddressSync } from './utils/pda'; import BN from 'bn.js'; import { Buffer } from 'buffer'; import { ConfirmOptions, PublicKey } from '@solana/web3.js'; @@ -102,7 +103,7 @@ export const getRegisteredProgramPda = () => new PublicKey('35hkDgaAKwMCaxRz2ocSZ6NaUrtKkyNqU6c4RV3tYJRh'); export const getAccountCompressionAuthority = () => - PublicKey.findProgramAddressSync( + findProgramAddressSync( [Buffer.from('cpi_authority')], new PublicKey(lightSystemProgram), )[0]; diff --git a/js/stateless.js/src/programs/system/program.ts b/js/stateless.js/src/programs/system/program.ts index 300ab1255d..be6e7aeac2 100644 --- a/js/stateless.js/src/programs/system/program.ts +++ b/js/stateless.js/src/programs/system/program.ts @@ -1,3 +1,4 @@ +import { findProgramAddressSync } from '../../utils/pda'; import BN from 'bn.js'; import { PublicKey, @@ -188,10 +189,7 @@ export class LightSystemProgram { */ static deriveCompressedSolPda(): PublicKey { const seeds = [SOL_POOL_PDA_SEED]; - const [address, _] = PublicKey.findProgramAddressSync( - seeds, - this.programId, - ); + const [address, _] = findProgramAddressSync(seeds, this.programId); return address; } diff --git a/js/stateless.js/src/utils/instruction.ts b/js/stateless.js/src/utils/instruction.ts index 9293945e34..794fe3c5f3 100644 --- a/js/stateless.js/src/utils/instruction.ts +++ b/js/stateless.js/src/utils/instruction.ts @@ -1,3 +1,4 @@ +import { findProgramAddressSync } from './pda'; import { AccountMeta, PublicKey, SystemProgram } from '@solana/web3.js'; import { defaultStaticAccountsStruct, featureFlags } from '../constants'; import { LightSystemProgram } from '../programs'; @@ -169,7 +170,7 @@ export function getLightSystemAccountMetasLegacy( config: SystemAccountMetaConfig, ): AccountMeta[] { let signerSeed = new TextEncoder().encode('cpi_authority'); - const cpiSigner = PublicKey.findProgramAddressSync( + const cpiSigner = findProgramAddressSync( [signerSeed], config.selfProgram, )[0]; @@ -244,7 +245,7 @@ export function getLightSystemAccountMetasV2( config: SystemAccountMetaConfig, ): AccountMeta[] { let signerSeed = new TextEncoder().encode('cpi_authority'); - const cpiSigner = PublicKey.findProgramAddressSync( + const cpiSigner = findProgramAddressSync( [signerSeed], config.selfProgram, )[0]; diff --git a/js/stateless.js/src/utils/pda.ts b/js/stateless.js/src/utils/pda.ts new file mode 100644 index 0000000000..1c46ed07f5 --- /dev/null +++ b/js/stateless.js/src/utils/pda.ts @@ -0,0 +1,63 @@ +import { PublicKey } from '@solana/web3.js'; +import { sha256 } from '@noble/hashes/sha256'; +import { Buffer } from 'buffer'; + +const MAX_SEED_LENGTH = 32; +const MAX_SEEDS = 16; +const PDA_MARKER_BYTES = new TextEncoder().encode('ProgramDerivedAddress'); + +export function createProgramAddressSync( + seeds: Array, + programId: PublicKey, +): PublicKey { + if (typeof PublicKey.createProgramAddressSync === 'function') { + return PublicKey.createProgramAddressSync(seeds, programId); + } + if (seeds.length > MAX_SEEDS) { + throw new TypeError('Max number of seeds exceeded'); + } + let buffer = Buffer.alloc(0); + for (const seed of seeds) { + if (seed.length > MAX_SEED_LENGTH) { + throw new TypeError('Max seed length exceeded'); + } + buffer = Buffer.concat([buffer, Buffer.from(seed)]); + } + const programIdBytes = + typeof programId.toBuffer === 'function' + ? programId.toBuffer() + : Buffer.from(programId.toBytes()); + buffer = Buffer.concat([ + buffer, + programIdBytes, + Buffer.from(PDA_MARKER_BYTES), + ]); + const publicKeyBytes = sha256(new Uint8Array(buffer)); + if (PublicKey.isOnCurve(publicKeyBytes)) { + throw new Error('Invalid seeds, address must fall off the curve'); + } + return new PublicKey(publicKeyBytes); +} + +export function findProgramAddressSync( + seeds: Array, + programId: PublicKey, +): [PublicKey, number] { + if (typeof PublicKey.findProgramAddressSync === 'function') { + return PublicKey.findProgramAddressSync(seeds, programId); + } + let nonce = 255; + while (nonce !== 0) { + try { + const seedsWithNonce = seeds.concat(Buffer.from([nonce])); + const address = createProgramAddressSync(seedsWithNonce, programId); + return [address, nonce]; + } catch (err) { + if (err instanceof TypeError) { + throw err; + } + nonce--; + } + } + throw new Error('Unable to find a viable program address nonce'); +} diff --git a/js/stateless.js/tests/unit/utils/packed-accounts.test.ts b/js/stateless.js/tests/unit/utils/packed-accounts.test.ts index ae30bf1618..7e7f0de3d3 100644 --- a/js/stateless.js/tests/unit/utils/packed-accounts.test.ts +++ b/js/stateless.js/tests/unit/utils/packed-accounts.test.ts @@ -1,6 +1,11 @@ import { describe, expect, it } from 'vitest'; import { PublicKey } from '@solana/web3.js'; -import { PackedAccounts } from '../../../src/utils/instruction'; +import { + PackedAccounts, + SystemAccountMetaConfig, +} from '../../../src/utils/instruction'; +import { getAccountCompressionAuthority } from '../../../src/constants'; +import { LightSystemProgram } from '../../../src/programs/system/program'; describe('PackedAccounts runtime boolean normalization', () => { it('normalizes numeric AccountMeta flags from addPreAccountsMeta', () => { @@ -36,4 +41,42 @@ describe('PackedAccounts runtime boolean normalization', () => { expect(meta.isSigner).toBe(true); expect(meta.isWritable).toBe(false); }); + + it('derives identical system accounts when PublicKey.findProgramAddressSync is unavailable (@solana/web3.js v3)', () => { + const selfProgram = new PublicKey('11111111111111111111111111111111'); + const config = SystemAccountMetaConfig.new(selfProgram); + + const expectedPacked = new PackedAccounts(); + expectedPacked.addSystemAccounts(config); + const expectedAuthority = getAccountCompressionAuthority(); + const expectedSolPda = LightSystemProgram.deriveCompressedSolPda(); + + const origFind = PublicKey.findProgramAddressSync; + const origCreate = PublicKey.createProgramAddressSync; + + try { + ( + PublicKey as unknown as Record + ).findProgramAddressSync = undefined; + ( + PublicKey as unknown as Record + ).createProgramAddressSync = undefined; + + const fallbackPacked = new PackedAccounts(); + fallbackPacked.addSystemAccounts(config); + + expect(fallbackPacked.toAccountMetas()).toEqual( + expectedPacked.toAccountMetas(), + ); + expect(getAccountCompressionAuthority().toBase58()).toBe( + expectedAuthority.toBase58(), + ); + expect(LightSystemProgram.deriveCompressedSolPda().toBase58()).toBe( + expectedSolPda.toBase58(), + ); + } finally { + PublicKey.findProgramAddressSync = origFind; + PublicKey.createProgramAddressSync = origCreate; + } + }); });