Skip to content
markpaper

src/signing/subaccount.test.ts

v0.2.0 · 3.6 KB

Download file
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts';
import { describe, expect, it } from 'vitest';
import {
  isBytes32,
  isValidSubaccountName,
  isZeroAddress,
  normalizeAddress,
  parseSubaccountBytes32,
  productVerifyingContract,
  signerBytes32,
  subaccountBytes32,
  ZERO_ADDRESS,
} from './subaccount.js';

const account = privateKeyToAccount(generatePrivateKey());
const addr = account.address.toLowerCase() as `0x${string}`;

describe('subaccountBytes32', () => {
  it('encodes address + "default" + zero padding (documentation vector shape)', () => {
    const b = subaccountBytes32(account.address, 'default');
    expect(b).toHaveLength(66);
    expect(b.startsWith(addr)).toBe(true);
    // 'default' = 64 65 66 61 75 6c 74, then 5 zero bytes
    expect(b.slice(42)).toBe(`64656661756c74${'0'.repeat(10)}`);
    expect(subaccountBytes32(account.address)).toBe(b); // default name
  });

  it('is case-insensitive on the address and round-trips through parseSubaccountBytes32', () => {
    const b = subaccountBytes32(account.address.toUpperCase().replace('0X', '0x'), 'bot1');
    expect(parseSubaccountBytes32(b)).toEqual({ address: addr, name: 'bot1' });
    expect(parseSubaccountBytes32(subaccountBytes32(addr, ''))).toEqual({ address: addr, name: '' });
    expect(parseSubaccountBytes32(subaccountBytes32(addr, 'abcdefghijkl')).name).toBe('abcdefghijkl');
  });

  it('a different name is a different account (bytes differ)', () => {
    expect(subaccountBytes32(addr, 'default')).not.toBe(subaccountBytes32(addr, 'Default'));
  });

  it('rejects malformed addresses and names', () => {
    expect(() => subaccountBytes32('0x123', 'default')).toThrow(TypeError);
    expect(() => subaccountBytes32(addr, 'thirteen chars')).toThrow(TypeError);
    expect(() => subaccountBytes32(addr, 'with space')).toThrow(TypeError);
    expect(() => subaccountBytes32(addr, 'café')).toThrow(TypeError);
    expect(isValidSubaccountName('default')).toBe(true);
    expect(isValidSubaccountName('abcdefghijklm')).toBe(false);
    expect(() => parseSubaccountBytes32('0x12')).toThrow(TypeError);
  });
});

describe('signerBytes32', () => {
  it('is the address followed by 12 zero bytes', () => {
    expect(signerBytes32(account.address)).toBe(`${addr}${'0'.repeat(24)}`);
    expect(signerBytes32(ZERO_ADDRESS)).toBe(`0x${'0'.repeat(64)}`);
  });
});

describe('productVerifyingContract', () => {
  it('writes the product id as a 20-byte address', () => {
    expect(productVerifyingContract(18)).toBe(`0x${'0'.repeat(38)}12`);
    expect(productVerifyingContract(2)).toBe(`0x${'0'.repeat(39)}2`);
    expect(productVerifyingContract(188)).toBe(`0x${'0'.repeat(38)}bc`);
    expect(productVerifyingContract(0)).toBe(ZERO_ADDRESS);
  });

  it('rejects invalid ids', () => {
    expect(() => productVerifyingContract(-1)).toThrow(RangeError);
    expect(() => productVerifyingContract(1.5)).toThrow(RangeError);
    expect(() => productVerifyingContract(Number.NaN)).toThrow(RangeError);
  });
});

describe('address helpers', () => {
  it('normalizeAddress lower-cases and validates', () => {
    expect(normalizeAddress(` ${account.address} `)).toBe(addr);
    expect(() => normalizeAddress('nope')).toThrow(TypeError);
    expect(() => normalizeAddress(1 as unknown as string)).toThrow(TypeError);
  });

  it('isZeroAddress / isBytes32', () => {
    expect(isZeroAddress(ZERO_ADDRESS)).toBe(true);
    expect(isZeroAddress(addr)).toBe(false);
    expect(isBytes32(`0x${'a'.repeat(64)}`)).toBe(true);
    expect(isBytes32(`0x${'A'.repeat(64)}`)).toBe(true);
    expect(isBytes32(`0x${'a'.repeat(63)}`)).toBe(false);
    expect(isBytes32(42)).toBe(false);
  });
});
All files