diff --git a/packages/cosmic-proto/src/address-hooks.js b/packages/cosmic-proto/src/address-hooks.js index 073f0783c71f..c4c39645c7dd 100644 --- a/packages/cosmic-proto/src/address-hooks.js +++ b/packages/cosmic-proto/src/address-hooks.js @@ -44,7 +44,7 @@ if ((ADDRESS_HOOK_VERSION & 0x0f) !== ADDRESS_HOOK_VERSION) { const ADDRESS_HOOK_MAGIC = new Uint8Array([ 0x78, 0xf1, - 0x70 | ADDRESS_HOOK_VERSION, + 0x70, // | ADDRESS_HOOK_VERSION ]); /** @@ -145,6 +145,7 @@ export const joinHookedAddress = ( magicLength + b + hd + BASE_ADDRESS_LENGTH_BYTES, ); hookBuf.set(ADDRESS_HOOK_MAGIC, 0); + hookBuf[magicLength - 1] |= ADDRESS_HOOK_VERSION; hookBuf.set(bytes, magicLength); hookBuf.set(hookData, magicLength + b); @@ -204,12 +205,23 @@ export const splitHookedAddressUnsafe = ( const { prefix, bytes } = decodeBech32(specimen, charLimit); const magicLength = ADDRESS_HOOK_MAGIC.length; + let version = 0xff; for (let i = 0; i < magicLength; i += 1) { - if (bytes[i] !== ADDRESS_HOOK_MAGIC[i]) { + let maybeMagicByte = bytes[i]; + if (i === magicLength - 1) { + // Final byte has a low version nibble and a high magic nibble. + version = maybeMagicByte & 0x0f; + maybeMagicByte &= 0xf0; + } + if (maybeMagicByte !== ADDRESS_HOOK_MAGIC[i]) { return { baseAddress: specimen, hookData: new Uint8Array() }; } } + if (version !== ADDRESS_HOOK_VERSION) { + return `Unsupported address hook version ${version}`; + } + let len = 0; for (let i = BASE_ADDRESS_LENGTH_BYTES - 1; i >= 0; i -= 1) { const byte = bytes.at(-i - 1); diff --git a/packages/cosmic-proto/test/address-hooks.test.js b/packages/cosmic-proto/test/address-hooks.test.js index 31ce2334f102..a216fc3159d5 100644 --- a/packages/cosmic-proto/test/address-hooks.test.js +++ b/packages/cosmic-proto/test/address-hooks.test.js @@ -42,6 +42,62 @@ test.before(async t => { t.context = await makeTestContext(); }); +/** + * @type {import('ava').Macro< + * [addressHook: string, baseAddress: string, hookDataStr: string, error?: any], + * { addressHooks: import('../src/address-hooks.js') } + * >} + */ +const splitMacro = test.macro({ + title(providedTitle = '', addressHook, _baseAddress, _hookDataStr, _error) { + return `${providedTitle} split ${addressHook}`; + }, + exec(t, addressHook, baseAddress, hookDataStr, error) { + const { splitHookedAddress } = t.context.addressHooks; + if (error) { + t.throws(() => splitHookedAddress(addressHook), error); + return; + } + const { baseAddress: ba, hookData: hd } = splitHookedAddress(addressHook); + t.is(ba, baseAddress); + const hookData = new TextEncoder().encode(hookDataStr); + t.deepEqual(hd, hookData); + }, +}); + +test('empty', splitMacro, '', '', '', { message: ' too short' }); +test('no hook', splitMacro, 'agoric1qqp0e5ys', 'agoric1qqp0e5ys', '', ''); +test( + 'Fast USDC', + splitMacro, + 'agoric10rchp4vc53apxn32q42c3zryml8xq3xshyzuhjk6405wtxy7tl3d7e0f8az423padaek6me38qekget2vdhx66mtvy6kg7nrw5uhsaekd4uhwufswqex6dtsv44hxv3cd4jkuqpqvduyhf', + 'agoric16kv2g7snfc4q24vg3pjdlnnqgngtjpwtetd2h689nz09lcklvh5s8u37ek', + '?EUD=osmo183dejcnmkka5dzcu9xw6mywq0p2m5peks28men', +); +test( + 'version 0', + splitMacro, + 'agoric10rchqqqpqgpsgpgxquyqjzstpsxsurcszyfpxpqrqgqsq9qx0p9wp', + 'agoric1qqqsyqcyq5rqwzqfpg9scrgwpugpzysn3tn9p0', + '\x04\x03\x02\x01', +); +test( + 'version 1 reject', + splitMacro, + 'agoric10rchzqqpqgpsgpgxquyqjzstpsxsurcszyfpxpqrqgqsq9q04n2fg', + '', + '', + { message: 'Unsupported address hook version 1' }, +); +test( + 'version 15 reject', + splitMacro, + 'agoric10rch7qqpqgpsgpgxquyqjzstpsxsurcszyfpxpqrqgqsq9q25ez2d', + '', + '', + { message: 'Unsupported address hook version 15' }, +); + /** * @type {import('ava').Macro< * [string, ArrayLike | undefined, ArrayLike, string],