diff --git a/js/compressed-token/eslint.config.cjs b/js/compressed-token/eslint.config.cjs index f47c9d54e1..ee3ffecc4f 100644 --- a/js/compressed-token/eslint.config.cjs +++ b/js/compressed-token/eslint.config.cjs @@ -1,108 +1,113 @@ -const js = require("@eslint/js"); -const tseslint = require("@typescript-eslint/eslint-plugin"); -const tsParser = require("@typescript-eslint/parser"); +const js = require('@eslint/js'); +const tseslint = require('@typescript-eslint/eslint-plugin'); +const tsParser = require('@typescript-eslint/parser'); module.exports = [ - { - ignores: [ - "node_modules/**", - "dist/**", - "build/**", - "coverage/**", - "*.config.js", - "eslint.config.js", - "jest.config.js", - "rollup.config.js", - ], - }, - js.configs.recommended, - { - files: ["**/*.js", "**/*.cjs", "**/*.mjs"], - languageOptions: { - ecmaVersion: 2022, - sourceType: "module", - globals: { - require: "readonly", - module: "readonly", - process: "readonly", - __dirname: "readonly", - __filename: "readonly", - exports: "readonly", - console: "readonly", - Buffer: "readonly", - }, + { + ignores: [ + 'node_modules/**', + 'dist/**', + 'build/**', + 'coverage/**', + '*.config.js', + 'eslint.config.js', + 'jest.config.js', + 'rollup.config.js', + ], }, - }, - { - files: ["tests/**/*.ts", "**/*.test.ts", "**/*.spec.ts", "vitest.config.ts"], - languageOptions: { - parser: tsParser, - parserOptions: { - ecmaVersion: 2022, - sourceType: "module", - }, - globals: { - process: "readonly", - console: "readonly", - __dirname: "readonly", - __filename: "readonly", - Buffer: "readonly", - describe: "readonly", - it: "readonly", - expect: "readonly", - beforeEach: "readonly", - afterEach: "readonly", - beforeAll: "readonly", - afterAll: "readonly", - jest: "readonly", - test: "readonly", - }, + js.configs.recommended, + { + files: ['**/*.js', '**/*.cjs', '**/*.mjs'], + languageOptions: { + ecmaVersion: 2022, + sourceType: 'module', + globals: { + require: 'readonly', + module: 'readonly', + process: 'readonly', + __dirname: 'readonly', + __filename: 'readonly', + exports: 'readonly', + console: 'readonly', + Buffer: 'readonly', + }, + }, }, - plugins: { - "@typescript-eslint": tseslint, + { + files: [ + 'tests/**/*.ts', + '**/*.test.ts', + '**/*.spec.ts', + 'vitest.config.ts', + ], + languageOptions: { + parser: tsParser, + parserOptions: { + ecmaVersion: 2022, + sourceType: 'module', + }, + globals: { + process: 'readonly', + console: 'readonly', + __dirname: 'readonly', + __filename: 'readonly', + Buffer: 'readonly', + describe: 'readonly', + it: 'readonly', + expect: 'readonly', + beforeEach: 'readonly', + afterEach: 'readonly', + beforeAll: 'readonly', + afterAll: 'readonly', + jest: 'readonly', + test: 'readonly', + }, + }, + plugins: { + '@typescript-eslint': tseslint, + }, + rules: { + ...tseslint.configs.recommended.rules, + '@typescript-eslint/ban-ts-comment': 0, + '@typescript-eslint/no-explicit-any': 0, + '@typescript-eslint/no-var-requires': 0, + '@typescript-eslint/no-unused-vars': 0, + '@typescript-eslint/no-require-imports': 0, + 'no-prototype-builtins': 0, + 'no-undef': 0, + 'no-unused-vars': 0, + }, }, - rules: { - ...tseslint.configs.recommended.rules, - "@typescript-eslint/ban-ts-comment": 0, - "@typescript-eslint/no-explicit-any": 0, - "@typescript-eslint/no-var-requires": 0, - "@typescript-eslint/no-unused-vars": 0, - "@typescript-eslint/no-require-imports": 0, - "no-prototype-builtins": 0, - "no-undef": 0, - "no-unused-vars": 0, + { + files: ['src/**/*.ts', 'src/**/*.tsx'], + languageOptions: { + parser: tsParser, + parserOptions: { + project: './tsconfig.json', + ecmaVersion: 2022, + sourceType: 'module', + }, + globals: { + process: 'readonly', + console: 'readonly', + __dirname: 'readonly', + __filename: 'readonly', + Buffer: 'readonly', + }, + }, + plugins: { + '@typescript-eslint': tseslint, + }, + rules: { + ...tseslint.configs.recommended.rules, + '@typescript-eslint/ban-ts-comment': 0, + '@typescript-eslint/no-explicit-any': 0, + '@typescript-eslint/no-var-requires': 0, + '@typescript-eslint/no-unused-vars': 0, + '@typescript-eslint/no-require-imports': 0, + 'no-prototype-builtins': 0, + 'no-undef': 0, // TypeScript handles this + 'no-unused-vars': 0, + }, }, - }, - { - files: ["src/**/*.ts", "src/**/*.tsx"], - languageOptions: { - parser: tsParser, - parserOptions: { - project: "./tsconfig.json", - ecmaVersion: 2022, - sourceType: "module", - }, - globals: { - process: "readonly", - console: "readonly", - __dirname: "readonly", - __filename: "readonly", - Buffer: "readonly", - }, - }, - plugins: { - "@typescript-eslint": tseslint, - }, - rules: { - ...tseslint.configs.recommended.rules, - "@typescript-eslint/ban-ts-comment": 0, - "@typescript-eslint/no-explicit-any": 0, - "@typescript-eslint/no-var-requires": 0, - "@typescript-eslint/no-unused-vars": 0, - "@typescript-eslint/no-require-imports": 0, - "no-prototype-builtins": 0, - "no-undef": 0, // TypeScript handles this - "no-unused-vars": 0, - }, - }, -]; \ No newline at end of file +]; diff --git a/js/stateless.js/eslint.config.cjs b/js/stateless.js/eslint.config.cjs index 3ee2f873a3..f44c0e1c89 100644 --- a/js/stateless.js/eslint.config.cjs +++ b/js/stateless.js/eslint.config.cjs @@ -1,112 +1,117 @@ -const js = require("@eslint/js"); -const tseslint = require("@typescript-eslint/eslint-plugin"); -const tsParser = require("@typescript-eslint/parser"); +const js = require('@eslint/js'); +const tseslint = require('@typescript-eslint/eslint-plugin'); +const tsParser = require('@typescript-eslint/parser'); module.exports = [ - { - ignores: [ - "node_modules/**", - "dist/**", - "build/**", - "coverage/**", - "*.config.js", - "eslint.config.js", - "jest.config.js", - "rollup.config.js", - "playwright-report/**", - ".playwright/**", - ], - }, - js.configs.recommended, - { - files: ["**/*.js", "**/*.cjs", "**/*.mjs"], - languageOptions: { - ecmaVersion: 2022, - sourceType: "module", - globals: { - require: "readonly", - module: "readonly", - process: "readonly", - __dirname: "readonly", - __filename: "readonly", - exports: "readonly", - console: "readonly", - Buffer: "readonly", - }, + { + ignores: [ + 'node_modules/**', + 'dist/**', + 'build/**', + 'coverage/**', + '*.config.js', + 'eslint.config.js', + 'jest.config.js', + 'rollup.config.js', + 'playwright-report/**', + '.playwright/**', + ], }, - }, - { - files: ["tests/**/*.ts", "**/*.test.ts", "**/*.spec.ts", "vitest.config.ts"], - languageOptions: { - parser: tsParser, - parserOptions: { - ecmaVersion: 2022, - sourceType: "module", - }, - globals: { - process: "readonly", - console: "readonly", - __dirname: "readonly", - __filename: "readonly", - Buffer: "readonly", - describe: "readonly", - it: "readonly", - expect: "readonly", - beforeEach: "readonly", - afterEach: "readonly", - beforeAll: "readonly", - afterAll: "readonly", - jest: "readonly", - test: "readonly", - }, + js.configs.recommended, + { + files: ['**/*.js', '**/*.cjs', '**/*.mjs'], + languageOptions: { + ecmaVersion: 2022, + sourceType: 'module', + globals: { + require: 'readonly', + module: 'readonly', + process: 'readonly', + __dirname: 'readonly', + __filename: 'readonly', + exports: 'readonly', + console: 'readonly', + Buffer: 'readonly', + }, + }, }, - plugins: { - "@typescript-eslint": tseslint, + { + files: [ + 'tests/**/*.ts', + '**/*.test.ts', + '**/*.spec.ts', + 'vitest.config.ts', + ], + languageOptions: { + parser: tsParser, + parserOptions: { + ecmaVersion: 2022, + sourceType: 'module', + }, + globals: { + process: 'readonly', + console: 'readonly', + __dirname: 'readonly', + __filename: 'readonly', + Buffer: 'readonly', + describe: 'readonly', + it: 'readonly', + expect: 'readonly', + beforeEach: 'readonly', + afterEach: 'readonly', + beforeAll: 'readonly', + afterAll: 'readonly', + jest: 'readonly', + test: 'readonly', + }, + }, + plugins: { + '@typescript-eslint': tseslint, + }, + rules: { + ...tseslint.configs.recommended.rules, + '@typescript-eslint/ban-ts-comment': 0, + '@typescript-eslint/no-explicit-any': 0, + '@typescript-eslint/no-var-requires': 0, + '@typescript-eslint/no-unused-vars': 0, + '@typescript-eslint/no-require-imports': 0, + 'no-prototype-builtins': 0, + 'no-undef': 0, + 'no-unused-vars': 0, + 'no-redeclare': 0, + }, }, - rules: { - ...tseslint.configs.recommended.rules, - "@typescript-eslint/ban-ts-comment": 0, - "@typescript-eslint/no-explicit-any": 0, - "@typescript-eslint/no-var-requires": 0, - "@typescript-eslint/no-unused-vars": 0, - "@typescript-eslint/no-require-imports": 0, - "no-prototype-builtins": 0, - "no-undef": 0, - "no-unused-vars": 0, - "no-redeclare": 0, + { + files: ['src/**/*.ts', 'src/**/*.tsx'], + languageOptions: { + parser: tsParser, + parserOptions: { + project: './tsconfig.json', + ecmaVersion: 2022, + sourceType: 'module', + }, + globals: { + process: 'readonly', + console: 'readonly', + __dirname: 'readonly', + __filename: 'readonly', + Buffer: 'readonly', + }, + }, + plugins: { + '@typescript-eslint': tseslint, + }, + rules: { + ...tseslint.configs.recommended.rules, + '@typescript-eslint/ban-ts-comment': 0, + '@typescript-eslint/no-explicit-any': 0, + '@typescript-eslint/no-var-requires': 0, + '@typescript-eslint/no-unused-vars': 0, + '@typescript-eslint/no-require-imports': 0, + 'no-prototype-builtins': 0, + 'no-undef': 0, // TypeScript handles this + 'no-unused-vars': 0, + 'no-redeclare': 0, + }, }, - }, - { - files: ["src/**/*.ts", "src/**/*.tsx"], - languageOptions: { - parser: tsParser, - parserOptions: { - project: "./tsconfig.json", - ecmaVersion: 2022, - sourceType: "module", - }, - globals: { - process: "readonly", - console: "readonly", - __dirname: "readonly", - __filename: "readonly", - Buffer: "readonly", - }, - }, - plugins: { - "@typescript-eslint": tseslint, - }, - rules: { - ...tseslint.configs.recommended.rules, - "@typescript-eslint/ban-ts-comment": 0, - "@typescript-eslint/no-explicit-any": 0, - "@typescript-eslint/no-var-requires": 0, - "@typescript-eslint/no-unused-vars": 0, - "@typescript-eslint/no-require-imports": 0, - "no-prototype-builtins": 0, - "no-undef": 0, // TypeScript handles this - "no-unused-vars": 0, - "no-redeclare": 0, - }, - }, -]; \ No newline at end of file +]; diff --git a/js/stateless.js/src/utils/address.ts b/js/stateless.js/src/utils/address.ts index 7d4a6ce074..fd5811b58a 100644 --- a/js/stateless.js/src/utils/address.ts +++ b/js/stateless.js/src/utils/address.ts @@ -1,7 +1,12 @@ import { PublicKey } from '@solana/web3.js'; -import { hashToBn254FieldSizeBe, hashvToBn254FieldSizeBe } from './conversion'; +import { + hashToBn254FieldSizeBe, + hashvToBn254FieldSizeBe, + hashvToBn254FieldSizeBeU8Array, +} from './conversion'; import { defaultTestStateTreeAccounts } from '../constants'; import { getIndexOrAdd } from '../programs/system/pack'; +import { keccak_256 } from '@noble/hashes/sha3'; export function deriveAddressSeed( seeds: Uint8Array[], @@ -12,7 +17,7 @@ export function deriveAddressSeed( return hash; } -/** +/* * Derive an address for a compressed account from a seed and an address Merkle * tree public key. * @@ -40,6 +45,42 @@ export function deriveAddress( return new PublicKey(buf); } +export function deriveAddressSeedV2(seeds: Uint8Array[]): Uint8Array { + const combinedSeeds: Uint8Array[] = seeds.map(seed => + Uint8Array.from(seed), + ); + const hash = hashvToBn254FieldSizeBeU8Array(combinedSeeds); + return hash; +} + +/** + * Derives an address from a seed using the v2 method (matching Rust's derive_address_from_seed) + * + * @param addressSeed The address seed (32 bytes) + * @param addressMerkleTreePubkey Merkle tree public key + * @param programId Program ID + * @returns Derived address + */ +export function deriveAddressV2( + addressSeed: Uint8Array, + addressMerkleTreePubkey: PublicKey, + programId: PublicKey, +): PublicKey { + if (addressSeed.length != 32) { + throw new Error('Address seed length is not 32 bytes.'); + } + const merkleTreeBytes = addressMerkleTreePubkey.toBytes(); + const programIdBytes = programId.toBytes(); + // Match Rust implementation: hash [seed, merkle_tree_pubkey, program_id] + const combined = [ + Uint8Array.from(addressSeed), + Uint8Array.from(merkleTreeBytes), + Uint8Array.from(programIdBytes), + ]; + const hash = hashvToBn254FieldSizeBeU8Array(combined); + return new PublicKey(hash); +} + export interface NewAddressParams { /** * Seed for the compressed account. Must be seed used to derive diff --git a/js/stateless.js/src/utils/conversion.ts b/js/stateless.js/src/utils/conversion.ts index 718ed43a61..2343b545bd 100644 --- a/js/stateless.js/src/utils/conversion.ts +++ b/js/stateless.js/src/utils/conversion.ts @@ -78,6 +78,19 @@ export function hashToBn254FieldSizeBe(bytes: Buffer): [Buffer, number] | null { return null; } +export function hashvToBn254FieldSizeBeU8Array( + bytes: Uint8Array[], +): Uint8Array { + const hasher = keccak_256.create(); + for (const input of bytes) { + hasher.update(input); + } + hasher.update(Uint8Array.from([255])); + const hash = hasher.digest(); + hash[0] = 0; + return hash; +} + /** * Hash the provided `bytes` with Keccak256 and ensure that the result fits in * the BN254 prime field by truncating the resulting hash to 31 bytes. diff --git a/js/stateless.js/tests/unit/utils/address-v2.test.ts b/js/stateless.js/tests/unit/utils/address-v2.test.ts new file mode 100644 index 0000000000..0c676f8aa9 --- /dev/null +++ b/js/stateless.js/tests/unit/utils/address-v2.test.ts @@ -0,0 +1,76 @@ +import { describe, it, expect } from 'vitest'; +import { PublicKey } from '@solana/web3.js'; +import { + deriveAddressSeedV2, + deriveAddressV2, +} from '../../../src/utils/address'; + +describe('V2 Address Derivation - Rust Compatibility Tests', () => { + const programId = new PublicKey( + '7yucc7fL3JGbyMwg4neUaenNSdySS39hbAk89Ao3t1Hz', + ); + const addressTreePubkey = new PublicKey(new Uint8Array(32).fill(0)); + + // Test vectors from Rust implementation + const testCases = [ + { + name: '["foo", "bar"]', + seeds: ['foo', 'bar'], + expectedSeed: [ + 0, 177, 134, 198, 24, 76, 116, 207, 56, 127, 189, 181, 87, 237, + 154, 181, 246, 54, 131, 21, 150, 248, 106, 75, 26, 80, 147, 245, + 3, 23, 136, 56, + ], + expectedAddress: [ + 0, 16, 227, 141, 38, 32, 23, 82, 252, 50, 202, 3, 183, 186, 236, + 133, 86, 112, 59, 23, 128, 162, 11, 84, 91, 127, 179, 208, 25, + 178, 1, 240, + ], + }, + { + name: '["ayy", "lmao"]', + seeds: ['ayy', 'lmao'], + expectedSeed: [ + 0, 224, 206, 65, 137, 189, 70, 157, 163, 133, 247, 140, 198, + 252, 169, 250, 18, 18, 16, 189, 164, 131, 225, 113, 197, 225, + 64, 81, 175, 154, 221, 28, + ], + expectedAddress: [ + 0, 226, 28, 142, 199, 153, 126, 212, 37, 54, 82, 232, 244, 161, + 108, 12, 67, 84, 111, 66, 107, 111, 8, 126, 153, 233, 239, 192, + 83, 117, 25, 6, + ], + }, + ]; + + describe('deriveAddressSeedV2', () => { + testCases.forEach(({ name, seeds, expectedSeed }) => { + it(`should match Rust for ${name}`, () => { + const seedBytes = seeds.map(s => new TextEncoder().encode(s)); + const addressSeed = deriveAddressSeedV2(seedBytes); + expect(addressSeed).toStrictEqual(new Uint8Array(expectedSeed)); + }); + }); + }); + + describe('deriveAddressV2', () => { + testCases.forEach(({ name, seeds, expectedSeed, expectedAddress }) => { + it(`should match Rust for ${name}`, () => { + const seedBytes = seeds.map(s => new TextEncoder().encode(s)); + const addressSeed = deriveAddressSeedV2(seedBytes); + + expect(addressSeed).toStrictEqual(new Uint8Array(expectedSeed)); + + const derivedAddress = deriveAddressV2( + addressSeed, + addressTreePubkey, + programId, + ); + + expect(derivedAddress.toBytes()).toStrictEqual( + new Uint8Array(expectedAddress), + ); + }); + }); + }); +});