Files
wikideck/server/market/xmr.ts
T
2026-10-20 10:06:38 -04:00

31 lines
2.0 KiB
TypeScript
Executable File

import { keccak } from 'hash-wasm';
const alphabet='123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
const decodedSizes:Record<number,number>={2:1,3:2,5:3,6:4,7:5,9:6,10:7,11:8};
/** Support only mainnet standard/subaddresses. Integrated/payment-ID addresses need a separate policy. */
export async function validateXmrAddress(address:string):Promise<string>{
if(typeof address!=='string'||address.length!==95)throw new Error('Expected a mainnet standard XMR address or subaddress');
const bytes:number[]=[];
for(let start=0;start<address.length;start+=11){
const chunk=address.slice(start,start+11),size=decodedSizes[chunk.length];
if(!size)throw new Error('Invalid XMR Base58 block');
let value=0n;
for(const c of chunk){const index=alphabet.indexOf(c);if(index<0)throw new Error('Invalid XMR Base58 alphabet');value=value*58n+BigInt(index);}
const output=Array<number>(size).fill(0);
for(let i=size-1;i>=0;i--){output[i]=Number(value&255n);value>>=8n;}
if(value)throw new Error('XMR Base58 block overflow');bytes.push(...output);
}
if(bytes.length!==69||![18,42].includes(bytes[0]))throw new Error('XMR address network/type is not supported');
const decoded=Uint8Array.from(bytes),expected=(await keccak(decoded.slice(0,-4),256)).slice(0,8);
if(Buffer.from(decoded.slice(-4)).toString('hex')!==expected)throw new Error('XMR checksum mismatch');
return address;
}
export function atomicXmr(value:string):bigint{
if(typeof value!=='string'||!/^(0|[1-9]\d{0,9})(\.\d{1,12})?$/.test(value))throw new Error('Invalid bounded XMR decimal');
const [whole,fraction='']=value.split('.');return BigInt(whole)*1_000_000_000_000n+BigInt(fraction.padEnd(12,'0'));
}
export function fiatCents(value:string):number{
if(typeof value!=='string'||!/^(0|[1-9]\d{0,6})(\.\d{1,2})?$/.test(value))throw new Error('Invalid USD decimal');
const [whole,fraction='']=value.split('.');return Number(whole)*100+Number(fraction.padEnd(2,'0'));
}