deviant/test/unit/web/api.test.ts
walerikus 518a3ce174 chore: initial project baseline — full Deviant codebase under version control
Brings the whole project into git (previously only the on-ramp feature and
a few parallel files were tracked). Includes: wallet core (src/), CashScript
contracts/, the Capacitor shell (shells/), scripts/, tests, and the docs
corpus (wiki/, snapshots/, Parameters/, Archive/, KnowledgeBase text).

Excluded via .gitignore: node_modules/, dist/, .env (secrets), testnet WIF
key files (.chipnet-*.json, devWallets.ts), KnowledgeBase/RAW media (~9.6 GB),
transcripts/ (session logs), and machine-local config (.claude/*.local,
*.lock). Secret-scanned before commit — no credentials in the tree.

Note: src/swapMath.ts, src/swapBuilder.ts, contracts/OpenMintToken.cash are
legacy (OP_RETURN era) but still imported; committed as-is, flagged for a
separate removal once de-referenced.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 20:38:07 +02:00

213 lines
8 KiB
TypeScript

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { jsonSafe, handleBalance, handleCommand, handleRegister, handleTokens, handleFavorite, handleForget } from '../../../src/web/api.js';
import { InMemoryTokenRegistry } from '../../../src/token/registry.js';
import type { TokenTierA } from '../../../src/token/types.js';
import type { DeviantWallet } from '../../../src/wallet/wallet.js';
import type { RunContext } from '../../../src/cli/wallet-cli.js';
const CAT_A = 'aa'.repeat(32);
function tokenA(over: Partial<TokenTierA> = {}): TokenTierA {
const id = over.id ?? CAT_A;
return {
id, tier: 'A', name: 'ASM', ticker: 'ASM', rateSats: 100n,
createdAt: 1, lastSeen: 1, userVerified: false, pinned: false, hidden: false,
categoryId: id, founderFeeBps: 100,
founderLockingBytecodeHex: '76a914' + '11'.repeat(20) + '88ac',
founderAddress: 'bchtest:qrf', contractAddress: 'bchtest:pc', contractTokenAddress: 'bchtest:rc',
contractBytecodeHash: '99'.repeat(32), initialReservoirSupply: '1', genesisTxId: '11'.repeat(32),
...over,
};
}
function mockWallet(record: { calls: string[] }): DeviantWallet {
return {
async getBalance() {
record.calls.push('getBalance');
return {
bchSats: 12345n,
tokens: [
{ kind: 'tier_a', token: tokenA(), verification: 'canonical', unitsRedeemable: 50n,
unitsUnderbacked: 0n, assetClassLabel: 'DEA', warnings: [] },
],
unknownTokens: [],
} as any;
},
async planClaim(intent: any) {
record.calls.push(`planClaim:${intent.units}`);
return { confirmationMessage: 'CLAIM PLAN', warnings: [] } as any;
},
async executeClaim() {
record.calls.push('executeClaim');
return { txid: 'cc'.repeat(32) };
},
} as unknown as DeviantWallet;
}
function ctx(record: { calls: string[] }): RunContext {
const registry = new InMemoryTokenRegistry();
registry.add(tokenA());
return { wallet: mockWallet(record), registry, ownTokenAddress: 'bchtest:rself' };
}
// ---- jsonSafe ----
test('jsonSafe: stringifies bigints recursively (objects, arrays, maps)', () => {
const input = {
a: 10n,
b: [1n, 2n, 'x'],
c: { d: 3n, e: false },
m: new Map<string, bigint>([['k', 9n]]),
};
const out = jsonSafe(input) as any;
assert.equal(out.a, '10');
assert.deepEqual(out.b, ['1', '2', 'x']);
assert.equal(out.c.d, '3');
assert.equal(out.c.e, false);
assert.equal(out.m.k, '9');
// Round-trips through JSON without throwing.
assert.doesNotThrow(() => JSON.stringify(out));
});
// ---- handleBalance ----
test('handleBalance: returns JSON-safe structured balance', async () => {
const record = { calls: [] as string[] };
const balance = (await handleBalance(ctx(record))) as any;
assert.equal(balance.bchSats, '12345');
assert.equal(balance.tokens[0].ticker ?? balance.tokens[0].token.ticker, 'ASM');
assert.equal(balance.tokens[0].unitsRedeemable, '50');
assert.equal(balance.tokens[0].verification, 'canonical');
assert.doesNotThrow(() => JSON.stringify(balance));
assert.deepEqual(record.calls, ['getBalance']);
});
// ---- handleCommand ----
test('handleCommand: previews claim (no broadcast) when yes flag absent', async () => {
const record = { calls: [] as string[] };
const res = await handleCommand(ctx(record), { command: 'claim', args: ['ASM', '1000'] });
assert.equal(res.previewOnly, true);
assert.equal(res.txid, undefined);
assert.match(res.output, /CLAIM PLAN/);
assert.ok(record.calls.includes('planClaim:1000'));
assert.ok(!record.calls.includes('executeClaim'));
});
test('handleCommand: broadcasts when yes flag set', async () => {
const record = { calls: [] as string[] };
const res = await handleCommand(ctx(record), {
command: 'claim',
args: ['ASM', '1000'],
flags: { yes: true },
});
assert.equal(res.previewOnly, false);
assert.equal(res.txid, 'cc'.repeat(32));
assert.ok(record.calls.includes('executeClaim'));
});
test('handleCommand: throws when command missing', async () => {
await assert.rejects(() => handleCommand(ctx({ calls: [] }), {} as any), /command is required/);
});
// ---- handleRegister ----
const portableTierB = {
version: 1, id: 'tb-web-1', tier: 'B', name: 'WebChips', ticker: 'WCHIP',
rateSats: '500', createdAt: 1, lastSeen: 1,
userVerified: false, pinned: false, hidden: false,
associatedAddresses: ['bchtest:qweb1'],
};
test('handleRegister: adds a token (object body) and persists it', async () => {
const c = ctx({ calls: [] });
const persisted: string[] = [];
const res = (await handleRegister(c, { definition: portableTierB }, (t) => persisted.push(t.id))) as any;
assert.equal(res.tier, 'B');
assert.equal(res.action, 'added');
assert.ok(res.confirmation, 'tier-B carries the D-5 confirmation');
assert.deepEqual(persisted, ['tb-web-1']);
assert.ok(c.registry.get('tb-web-1'), 'token is in the registry');
assert.doesNotThrow(() => JSON.stringify(res)); // bigints stringified
});
test('handleRegister: accepts a JSON string body', async () => {
const c = ctx({ calls: [] });
const res = (await handleRegister(c, { definition: JSON.stringify(portableTierB) })) as any;
assert.equal(res.token.id, 'tb-web-1');
});
test('handleRegister: rejects missing definition', async () => {
await assert.rejects(() => handleRegister(ctx({ calls: [] }), {} as any), /definition is required/);
});
test('handleRegister: rejects invalid JSON string', async () => {
await assert.rejects(
() => handleRegister(ctx({ calls: [] }), { definition: '{not json' }),
/not valid JSON/,
);
});
// ---- handleTokens (ASM catalog) ----
test('handleTokens: catalog entry per registry token, with rate + classification', () => {
const cat = handleTokens(ctx({ calls: [] })) as any[];
assert.ok(Array.isArray(cat));
const asm = cat.find((t) => t.id === CAT_A);
assert.ok(asm, 'registered token appears in the catalog');
assert.equal(asm.ticker, 'ASM');
assert.equal(asm.tier, 'A');
assert.equal(asm.rateSats, '100'); // bigint stringified
assert.equal(typeof asm.verification, 'string');
// Placeholder contract address can't be canonical → not ASM-eligible, no class.
assert.equal(asm.asm, 'none');
assert.equal(asm.assetClass, null);
assert.doesNotThrow(() => JSON.stringify(cat));
});
// ---- handleFavorite ----
test('handleFavorite: pins a token, persists, and reflects in the registry', () => {
const c = ctx({ calls: [] });
const persisted: string[] = [];
const res = handleFavorite(c, { id: CAT_A, pinned: true }, (t) => persisted.push(t.id)) as any;
assert.deepEqual(res, { id: CAT_A, pinned: true });
assert.equal(c.registry.get(CAT_A)!.pinned, true);
assert.deepEqual(persisted, [CAT_A]);
});
test('handleFavorite: unpins (pinned=false)', () => {
const c = ctx({ calls: [] });
handleFavorite(c, { id: CAT_A, pinned: true });
const res = handleFavorite(c, { id: CAT_A, pinned: false }) as any;
assert.equal(res.pinned, false);
assert.equal(c.registry.get(CAT_A)!.pinned, false);
});
test('handleFavorite: rejects missing id', () => {
assert.throws(() => handleFavorite(ctx({ calls: [] }), {} as any), /id is required/);
});
// ---- handleForget ("Convert to BCH") ----
test('handleForget: removes the token and drops its persisted file', () => {
const c = ctx({ calls: [] });
const dropped: string[] = [];
const res = handleForget(c, { id: CAT_A }, (id) => dropped.push(id)) as any;
assert.deepEqual(res, { id: CAT_A, removed: true });
assert.equal(c.registry.get(CAT_A), undefined);
assert.deepEqual(dropped, [CAT_A]);
});
test('handleForget: rejects missing id', () => {
assert.throws(() => handleForget(ctx({ calls: [] }), {} as any), /id is required/);
});
test('handleForget: throws when the id is not registered', () => {
assert.throws(() => handleForget(ctx({ calls: [] }), { id: 'nope' }), /not found/);
});
test('handleFavorite: propagates unknown-id error from the registry', () => {
assert.throws(() => handleFavorite(ctx({ calls: [] }), { id: 'nope', pinned: true }), /not found/);
});