diff --git a/src/client/utils/computer.ts b/src/client/utils/computer.ts index 26ffb7ee..33e4b433 100644 --- a/src/client/utils/computer.ts +++ b/src/client/utils/computer.ts @@ -11,7 +11,7 @@ export const OperationTypeUserDeposit = 3; export const userIdToBytes = (uid: string) => { // User IDs are uint64 decimal strings. Reject hex/exponent/whitespace that // BigNumber would otherwise coerce (e.g. '0x10' -> 16, '1e3' -> 1000). - if (typeof uid === 'string' && !/^\d+$/.test(uid)) { + if (typeof uid !== 'string' || !/^\d+$/.test(uid)) { throw new Error(`invalid user id: ${uid}`); } let x: BigNumber; diff --git a/src/client/utils/encoder.ts b/src/client/utils/encoder.ts index 00c1ddee..33361e9f 100644 --- a/src/client/utils/encoder.ts +++ b/src/client/utils/encoder.ts @@ -175,14 +175,13 @@ export class Encoder { o.keys.forEach(k => { // Buffer.from(k, 'hex') silently drops non-hex chars, so a typo'd key // would shift the whole framing and burn funds to a wrong key. - const kb = Buffer.from(k, 'hex'); - if (kb.byteLength !== 32) throw new Error(`invalid output key ${k}`); - this.write(kb); + if (!/^[0-9a-fA-F]{64}$/.test(k)) throw new Error(`invalid output key ${k}`); + this.write(Buffer.from(k, 'hex')); }); const maskHex = o.mask || ''; + if (maskHex && !/^[0-9a-fA-F]{64}$/.test(maskHex)) throw new Error(`invalid output mask ${o.mask}`); const mask = maskHex ? Buffer.from(maskHex, 'hex') : Buffer.alloc(32, 0); - if (mask.byteLength !== 32) throw new Error(`invalid output mask ${o.mask}`); this.write(mask); const scriptHex = o.script || ''; diff --git a/test/mixin/codec.test.ts b/test/mixin/codec.test.ts index 67b8b7b6..8a94517f 100644 --- a/test/mixin/codec.test.ts +++ b/test/mixin/codec.test.ts @@ -180,7 +180,7 @@ describe('transaction codec', () => { expect(() => encoder.encodeOutput({ type, amount: '1', keys: [] })).toThrow('invalid output type'); }); - it.each([['zz'], ['44'.repeat(31)], ['abc']])('rejects an output with a malformed key: %s', key => { + it.each(['zz', '44'.repeat(31), 'abc', `${'44'.repeat(32)}f`, `${'44'.repeat(32)}zz`])('rejects an output with a malformed key: %s', key => { const encoder = new Encoder(Buffer.alloc(0)); // Buffer.from(key, 'hex') silently truncates, which would shift the @@ -188,12 +188,21 @@ describe('transaction codec', () => { expect(() => encoder.encodeOutput({ amount: '1', keys: [key] })).toThrow('invalid output key'); }); - it.each(['zz', '44'.repeat(31)])('rejects an output with a malformed mask: %s', mask => { + it.each(['zz', '44'.repeat(31), `${'44'.repeat(32)}f`, `${'44'.repeat(32)}zz`])('rejects an output with a malformed mask: %s', mask => { const encoder = new Encoder(Buffer.alloc(0)); expect(() => encoder.encodeOutput({ amount: '1', keys: [], mask })).toThrow('invalid output mask'); }); + it.each([undefined, '', 'Ab'.repeat(32)])('accepts uppercase hex and an optional mask: %s', mask => { + const encoder = new Encoder(Buffer.alloc(0)); + encoder.encodeOutput({ amount: '1', keys: ['Ab'.repeat(32)], mask }); + expect(new Decoder(encoder.buffer()).decodeOutput()).toMatchObject({ + keys: ['ab'.repeat(32)], + mask: mask ? 'ab'.repeat(32) : '00'.repeat(32), + }); + }); + it('round-trips sorted signature entries', () => { const signatures = { 2: '22'.repeat(64), 0: '11'.repeat(64) }; const encoder = new Encoder(Buffer.alloc(0)); diff --git a/test/mixin/computer.test.ts b/test/mixin/computer.test.ts index 1b8e416a..7aca4c1d 100644 --- a/test/mixin/computer.test.ts +++ b/test/mixin/computer.test.ts @@ -31,6 +31,10 @@ describe('computer utilities', () => { expect(checkSystemCallSize(Buffer.alloc(MAX_SOLANA_TX_SIZE + 1))).toBe(false); }); + it.each([1, Number.MAX_SAFE_INTEGER + 1, BigInt(1), null, undefined, true])('rejects a non-string user ID: %s', userID => { + expect(() => userIdToBytes(userID as unknown as string)).toThrow('invalid user id'); + }); + it('builds system call extras with and without a fee ID', () => { const base = Buffer.concat([userIdToBytes('1'), Buffer.from(parseUUID(callID)), Buffer.from([1])]);