import { Bytes } from 'ox' import { VirtualMaster } from 'ox/tempo' import { describe, expect, test } from 'vp/test' import { instantiate } from '../wasm/internal/instantiate.js' import { wasmBase64 } from './internal/mine.wasm.js' const address = '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266' const salt = '0x00000000000000000000000000000000000000000000000000000000abf52baf' const masterId = '0x58e21090' const registrationHash = '0x0000000058e21090d8f4bee424b90cddc2378aefa1bbbfa1443631a929ae966d' const virtualAddress = '0x58e21090fdfdfdfdfdfdfdfdfdfd010203040506' const tip20Address = '0x20c0000000000000000000000000000000000001' describe('getRegistrationHash', () => { test('raw address', () => { expect( VirtualMaster.getRegistrationHash({ address, salt, }), ).toBe(registrationHash) }) }) describe('getMasterId', () => { test('default', () => { expect( VirtualMaster.getMasterId({ address, salt, }), ).toBe(masterId) }) }) describe('validateSalt', () => { test('returns true for valid salt', () => { expect(VirtualMaster.validateSalt({ address, salt })).toBe(true) }) test('returns false for invalid salt', () => { expect(VirtualMaster.validateSalt({ address, salt: 0n })).toBe(false) }) }) describe('mineSalt', () => { test('finds the first salt in range with the default keccak path', () => { expect( VirtualMaster.mineSalt({ address, count: 16, start: 0xabf52ba0n, }), ).toMatchInlineSnapshot(` { "masterId": "0x58e21090", "registrationHash": "0x0000000058e21090d8f4bee424b90cddc2378aefa1bbbfa1443631a929ae966d", "salt": "0x00000000000000000000000000000000000000000000000000000000abf52baf", } `) }) test('returns undefined when no salt is found in range', () => { expect( VirtualMaster.mineSalt({ address, count: 1, start: 0n, }), ).toBeUndefined() }) test('throws for a non-integer count', () => { expect(() => VirtualMaster.mineSalt({ address, count: 1.5, }), ).toThrowErrorMatchingInlineSnapshot( `[BaseError: Count "1.5" is invalid. Expected a positive safe integer.]`, ) }) test('throws for a non-finite count', () => { expect(() => VirtualMaster.mineSalt({ address, count: Number.POSITIVE_INFINITY, }), ).toThrowErrorMatchingInlineSnapshot( `[BaseError: Count "Infinity" is invalid. Expected a positive safe integer.]`, ) }) test.each([ ['zero address', '0x0000000000000000000000000000000000000000'], ['virtual address', virtualAddress], ['TIP-20 token address', tip20Address], ])('rejects %s as a virtual master', (_label, invalidAddress) => { expect(() => VirtualMaster.mineSalt({ address: invalidAddress as VirtualMaster.mineSalt.Value['address'], count: 1, }), ).toThrowError() }) }) describe('mineSaltAsync', () => { test('finds the same salt as the sync version', async () => { const result = await VirtualMaster.mineSaltAsync({ address, count: 16, start: 0xabf52ba0n, workers: 1, }) expect(result).toMatchInlineSnapshot(` { "masterId": "0x58e21090", "registrationHash": "0x0000000058e21090d8f4bee424b90cddc2378aefa1bbbfa1443631a929ae966d", "salt": "0x00000000000000000000000000000000000000000000000000000000abf52baf", } `) }) // `workerCount` is `min(workers, ceil(count / chunkSize))`, so the cases // above collapse to one worker and take the JavaScript fallback. Only a // chunk size small enough to need two workers reaches the WASM, which is how // a miner writing over its own Keccak constants went unnoticed. test('agrees with the fallback once the WASM actually runs', async () => { const viaFallback = await VirtualMaster.mineSaltAsync({ address, count: 16, start: 0xabf52ba0n, workers: 1, }) const viaWasm = await VirtualMaster.mineSaltAsync({ address, chunkSize: 8, count: 16, start: 0xabf52ba0n, workers: 2, }) expect(viaWasm).toEqual(viaFallback) expect(viaWasm?.salt).toMatchInlineSnapshot( `"0x00000000000000000000000000000000000000000000000000000000abf52baf"`, ) }) test('finds the same salt with workers', async () => { const result = await VirtualMaster.mineSaltAsync({ address, count: 16, start: 0xabf52ba0n, workers: 2, }) expect(result).toEqual({ masterId: '0x58e21090', registrationHash: '0x0000000058e21090d8f4bee424b90cddc2378aefa1bbbfa1443631a929ae966d', salt: '0x00000000000000000000000000000000000000000000000000000000abf52baf', }) }) test('returns undefined when no salt is found', async () => { const result = await VirtualMaster.mineSaltAsync({ address, count: 1, start: 0n, workers: 1, }) expect(result).toBeUndefined() }) test.each([ 0, -1, 1.5, Number.NaN, Number.POSITIVE_INFINITY, 2 ** 32, Number.MAX_SAFE_INTEGER + 1, ])('rejects chunk size %s outside the uint32 ABI', async (chunkSize) => { await expect( VirtualMaster.mineSaltAsync({ address, chunkSize, count: 1, workers: 1, }), ).rejects.toThrow( `Chunk size "${chunkSize}" is invalid. Expected a positive safe integer no greater than 4294967295.`, ) }) test('accepts the maximum uint32 chunk size', async () => { await expect( VirtualMaster.mineSaltAsync({ address, chunkSize: 2 ** 32 - 1, count: 1, start: 0n, workers: 1, }), ).resolves.toBeUndefined() }) test('treats the raw WASM count as unsigned', async () => { type Exports = { mine(count: number): number } const module = await instantiate(wasmBase64) module.reserve(84) module.view().set(Bytes.fromHex(address), module.heapBase) module.view().set(Bytes.fromHex(salt), module.heapBase + 20) // 2 ** 31 has its sign bit set in the i32 passed by JavaScript. The first // salt is already known to match, so unsigned C returns immediately; the old // signed ABI interpreted the count as negative and skipped the loop. expect(module.exports.mine(2 ** 31)).toBe(1) }) test('reports progress', async () => { const progressCalls: VirtualMaster.mineSaltAsync.Progress[] = [] await VirtualMaster.mineSaltAsync({ address, count: 16, start: 0xabf52ba0n, workers: 1, chunkSize: 8, onProgress: (p) => progressCalls.push({ ...p }), }) expect(progressCalls.length).toBeGreaterThanOrEqual(1) expect(progressCalls[0]!.workers).toBe(1) expect(progressCalls[0]!.attempts).toBeGreaterThan(0) }) test('supports AbortSignal cancellation', async () => { const controller = new AbortController() controller.abort() await expect( VirtualMaster.mineSaltAsync({ address, count: 1_000_000, signal: controller.signal, }), ).rejects.toThrow() }) }) test('exports', () => { expect(Object.keys(VirtualMaster)).toMatchInlineSnapshot(` [ "getRegistrationHash", "getMasterId", "validateSalt", "mineSalt", "mineSaltAsync", ] `) })