UNPKG

ox

Version:

Ethereum Standard Library

773 lines (665 loc) 21 kB
import { Attribution } from 'ox/erc8021' import { describe, expect, test } from 'vp/test' describe('getSchemaId', () => { test('returns 0 for canonical registry (no codeRegistry)', () => { const schemaId = Attribution.getSchemaId({ codes: ['baseapp'], }) expect(schemaId).toBe(0) }) test('returns 0 for canonical registry (explicit id: 0)', () => { const schemaId = Attribution.getSchemaId({ codes: ['baseapp'], id: 0, }) expect(schemaId).toBe(0) }) test('returns 1 for custom registry', () => { const schemaId = Attribution.getSchemaId({ codes: ['baseapp'], codeRegistry: { address: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', chainId: 1, }, }) expect(schemaId).toBe(1) }) test('returns 1 for custom registry (explicit id: 1)', () => { const schemaId = Attribution.getSchemaId({ codes: ['baseapp'], codeRegistry: { address: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', chainId: 1, }, id: 1, }) expect(schemaId).toBe(1) }) }) describe('toDataSuffix', () => { describe('schema 0 (canonical registry)', () => { test('single code', () => { const suffix = Attribution.toDataSuffix({ codes: ['baseapp'], }) // Expected: 'baseapp' (7 bytes) + length (1 byte: 0x07) + schema id (1 byte: 0x00) + erc suffix (16 bytes) // 'baseapp' = 0x626173656170702c expect(suffix).toBe( '0x62617365617070070080218021802180218021802180218021', ) }) test('multiple codes', () => { const suffix = Attribution.toDataSuffix({ codes: ['baseapp', 'morpho'], }) // Expected: 'baseapp,morpho' (14 bytes) + length (1 byte: 0x0e) + schema id (1 byte: 0x00) + erc suffix (16 bytes) // 'baseapp,morpho' = 0x626173656170702c6d6f7270686f (14 bytes) expect(suffix).toBe( '0x626173656170702c6d6f7270686f0e0080218021802180218021802180218021', ) }) test('explicit id', () => { const suffix = Attribution.toDataSuffix({ codes: ['test'], id: 0, }) // Expected: 'test' (4 bytes) + length (1 byte: 0x04) + schema id (1 byte: 0x00) + erc suffix (16 bytes) expect(suffix).toBe('0x74657374040080218021802180218021802180218021') }) test('three codes', () => { const suffix = Attribution.toDataSuffix({ codes: ['base', 'uniswap', 'cowswap'], }) // 'base,uniswap,cowswap' = 20 bytes expect(suffix).toMatchInlineSnapshot( `"0x626173652c756e69737761702c636f7773776170140080218021802180218021802180218021"`, ) }) test('single character code', () => { const suffix = Attribution.toDataSuffix({ codes: ['a'], }) // 'a' = 1 byte expect(suffix).toMatchInlineSnapshot( `"0x61010080218021802180218021802180218021"`, ) }) test('codes with numbers', () => { const suffix = Attribution.toDataSuffix({ codes: ['app123', 'test456'], }) // 'app123,test456' = 14 bytes expect(suffix).toMatchInlineSnapshot( `"0x6170703132332c746573743435360e0080218021802180218021802180218021"`, ) }) test('codes with special characters', () => { const suffix = Attribution.toDataSuffix({ codes: ['app-name', 'test_code'], }) // 'app-name,test_code' = 18 bytes expect(suffix).toMatchInlineSnapshot( `"0x6170702d6e616d652c746573745f636f6465120080218021802180218021802180218021"`, ) }) test('empty codes array', () => { const suffix = Attribution.toDataSuffix({ codes: [], }) // '' = 0 bytes expect(suffix).toMatchInlineSnapshot( `"0x000080218021802180218021802180218021"`, ) }) test('codes with mixed case preserved', () => { const suffix = Attribution.toDataSuffix({ codes: ['BaseApp', 'MorphoProtocol'], }) // 'BaseApp,MorphoProtocol' = 22 bytes expect(suffix).toMatchInlineSnapshot( `"0x426173654170702c4d6f7270686f50726f746f636f6c160080218021802180218021802180218021"`, ) }) test('spec example: single entity', () => { const suffix = Attribution.toDataSuffix({ codes: ['baseapp'], id: 0, }) // Expected segment after txData (0xdddddddd) // 'baseapp' (7 bytes) + length (0x07) + schema ID (0x00) + ERC suffix expect(suffix).toBe( '0x62617365617070070080218021802180218021802180218021', ) }) }) describe('schema 1 (custom registry)', () => { test('single code', () => { const input = { codes: ['baseapp'], codeRegistry: { address: '0xd8da6bf26964af9d7eed9e03e53415d37aa96045' as const, chainId: 1, }, id: 1 as const, } const suffix = Attribution.toDataSuffix(input) const parsed = Attribution.fromData( `0xdddddddd${suffix.slice(2)}` as const, ) expect(parsed).toEqual({ codes: ['baseapp'], codeRegistry: { address: input.codeRegistry.address.toLowerCase(), chainId: 1, }, id: 1, }) }) test('multiple codes', () => { const input = { codes: ['baseapp', 'morpho'], codeRegistry: { address: '0xd8da6bf26964af9d7eed9e03e53415d37aa96045' as const, chainId: 8453, }, id: 1 as const, } const suffix = Attribution.toDataSuffix(input) const parsed = Attribution.fromData( `0xeeeeeeee${suffix.slice(2)}` as const, ) expect(parsed).toEqual({ codes: ['baseapp', 'morpho'], codeRegistry: { address: input.codeRegistry.address.toLowerCase(), chainId: 8453, }, id: 1, }) }) test('single character code', () => { const input = { codes: ['x'], codeRegistry: { address: '0x1234567890123456789012345678901234567890' as const, chainId: 10, }, id: 1 as const, } const suffix = Attribution.toDataSuffix(input) const parsed = Attribution.fromData( `0xcccccccc${suffix.slice(2)}` as const, ) expect(parsed).toEqual({ codes: ['x'], codeRegistry: { address: input.codeRegistry.address.toLowerCase(), chainId: 10, }, id: 1, }) }) test('long codes', () => { const input = { codes: ['verylongapplicationname', 'anotherlongcode'], codeRegistry: { address: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd' as const, chainId: 42161, }, id: 1 as const, } const suffix = Attribution.toDataSuffix(input) const parsed = Attribution.fromData( `0xbbbbbbbb${suffix.slice(2)}` as const, ) expect(parsed).toEqual({ codes: ['verylongapplicationname', 'anotherlongcode'], codeRegistry: { address: input.codeRegistry.address.toLowerCase(), chainId: 42161, }, id: 1, }) }) test('spec example: multiple entities', () => { const input = { codes: ['baseapp', 'morpho'], codeRegistry: { address: '0xcccccccccccccccccccccccccccccccccccccccc' as const, chainId: 1, }, id: 1 as const, } const suffix = Attribution.toDataSuffix(input) const parsed = Attribution.fromData( `0xdddddddd${suffix.slice(2)}` as const, ) expect(parsed).toEqual({ codes: ['baseapp', 'morpho'], codeRegistry: { address: input.codeRegistry.address.toLowerCase(), chainId: 1, }, id: 1, }) }) }) }) describe('fromData', () => { describe('schema 0 (canonical registry)', () => { test('single entity', () => { // Input: transaction data + 'baseapp' (7 bytes) + length (0x07) + schema ID (0x00) + ERC suffix const input = '0xdddddddd62617365617070070080218021802180218021802180218021' const result = Attribution.fromData(input) expect(result).toEqual({ codes: ['baseapp'], id: 0, }) }) test('empty codes', () => { // Input: transaction data + empty codes (0 bytes) + length (0x00) + schema ID (0x00) + ERC suffix const input = '0xdddddddd000080218021802180218021802180218021' const result = Attribution.fromData(input) // Empty string splits to [''] not [] expect(result).toEqual({ codes: [], id: 0, }) }) test.each([ [['baseapp']], [['baseapp', 'morpho']], [['baseapp', 'morpho', 'uniswap']], ])('roundtrip{codes=%s}', (codes) => { const original: Attribution.Attribution = { codes: codes, } const suffix = Attribution.toDataSuffix(original) const fullData = `0xabcdef${suffix.slice(2)}` as const // Add some transaction data const parsed = Attribution.fromData(fullData) expect(parsed).toEqual({ codes: original.codes, id: 0, }) }) }) describe('schema 1 (custom registry)', () => { test('example from eip', () => { const encoded = '0xddddddddcccccccccccccccccccccccccccccccccccccccc210502626173656170702C6D6F7270686F0E0180218021802180218021802180218021' as const const expected = { codes: ['baseapp', 'morpho'], codeRegistry: { address: '0xcccccccccccccccccccccccccccccccccccccccc', chainId: 8453, }, id: 1, } as Attribution.AttributionSchemaId1 const decoded = Attribution.fromData(encoded) expect(decoded).toEqual(expected) }) test('multiple entities', () => { const suffix = Attribution.toDataSuffix({ codes: ['baseapp', 'morpho'], codeRegistry: { address: '0xcccccccccccccccccccccccccccccccccccccccc', chainId: 1, }, id: 1, }) const result = Attribution.fromData( `0xdddddddd${suffix.slice(2)}` as const, ) expect(result).toEqual({ codes: ['baseapp', 'morpho'], codeRegistry: { address: '0xcccccccccccccccccccccccccccccccccccccccc', chainId: 1, }, id: 1, }) }) test('single code', () => { const suffix = Attribution.toDataSuffix({ codes: ['x'], codeRegistry: { address: '0x1234567890123456789012345678901234567890', chainId: 10, }, id: 1, }) const result = Attribution.fromData( `0xdddddddd${suffix.slice(2)}` as const, ) expect(result).toEqual({ codes: ['x'], codeRegistry: { address: '0x1234567890123456789012345678901234567890', chainId: 10, }, id: 1, }) }) test.each([ 1, // 1 byte 8453, // 2 byte 84532, // 3 byte ])('roundtrip with different chain ids{chainId=%i}', (chainId) => { const original = { codes: ['baseapp', 'morpho'], codeRegistry: { address: '0xd8da6bf26964af9d7eed9e03e53415d37aa96045' as const, chainId, }, id: 1 as const, } const suffix = Attribution.toDataSuffix(original) const fullData = `0xabcdef${suffix.slice(2)}` as const // Add some transaction data const parsed = Attribution.fromData(fullData) expect(parsed).toEqual(original) }) }) describe('error cases', () => { test('invalid schemaId', () => { // Input: transaction data + length (0xff) + unknown schema ID (0xff) + ERC suffix const input = '0xddddddddff80218021802180218021802180218021' const result = Attribution.fromData(input) // Parsing stops, unknown schemaId returns undefined expect(result).toBeUndefined() }) test('missing ERC suffix', () => { // Input without valid ERC suffix const input = '0xdddddddd62617365617070070000000000000000000000000000000000000000' const result = Attribution.fromData(input) expect(result).toBeUndefined() }) test('data too short', () => { const input = '0xdddddddd' const result = Attribution.fromData(input) expect(result).toBeUndefined() }) }) }) describe('schema 2 (CBOR-encoded)', () => { describe('getSchemaId', () => { test('returns 2 for appCode', () => { const schemaId = Attribution.getSchemaId({ appCode: 'baseapp', }) expect(schemaId).toBe(2) }) test('returns 2 for walletCode', () => { const schemaId = Attribution.getSchemaId({ walletCode: 'privy', }) expect(schemaId).toBe(2) }) test('returns 2 for appCode + walletCode', () => { const schemaId = Attribution.getSchemaId({ appCode: 'baseapp', walletCode: 'privy', }) expect(schemaId).toBe(2) }) test('returns 2 for serviceCodes', () => { const schemaId = Attribution.getSchemaId({ serviceCodes: ['flashbots'], }) expect(schemaId).toBe(2) }) }) describe('toDataSuffix', () => { test('single app code', () => { const suffix = Attribution.toDataSuffix({ appCode: 'baseapp', }) // Verify it ends with schema ID 02 and ERC suffix expect(suffix.endsWith('0280218021802180218021802180218021')).toBe(true) // Roundtrip decode const parsed = Attribution.fromData(`0xdddddddd${suffix.slice(2)}`) expect(parsed).toEqual({ appCode: 'baseapp', id: 2, }) }) test('app + wallet codes', () => { const suffix = Attribution.toDataSuffix({ appCode: 'baseapp', walletCode: 'privy', }) const parsed = Attribution.fromData(`0xdddddddd${suffix.slice(2)}`) expect(parsed).toEqual({ appCode: 'baseapp', walletCode: 'privy', id: 2, }) }) test('with metadata', () => { const suffix = Attribution.toDataSuffix({ appCode: 'baseapp', metadata: { source: 'webapp', utm_campaign: 'winter-promo' }, }) const parsed = Attribution.fromData(`0xdddddddd${suffix.slice(2)}`) expect(parsed).toEqual({ appCode: 'baseapp', metadata: { source: 'webapp', utm_campaign: 'winter-promo' }, id: 2, }) }) test('with app registry', () => { const suffix = Attribution.toDataSuffix({ appCode: 'baseapp', registries: { app: { address: '0xBcf2B9598Ec0781eE49230CaACF0FB3C881B5195', chainId: 8453, }, }, }) const parsed = Attribution.fromData(`0xdddddddd${suffix.slice(2)}`) expect(parsed).toEqual({ appCode: 'baseapp', registries: { app: { address: '0xBcf2B9598Ec0781eE49230CaACF0FB3C881B5195', chainId: 8453, }, }, id: 2, }) }) test('full example: app, wallet, registry, metadata', () => { const input: Attribution.AttributionSchemaId2 = { appCode: 'baseapp', walletCode: 'privy', registries: { app: { address: '0xBcf2B9598Ec0781eE49230CaACF0FB3C881B5195', chainId: 8453, }, }, metadata: { utm_campaign: 'winter-promo', source: 'webapp', }, } const suffix = Attribution.toDataSuffix(input) const parsed = Attribution.fromData( `0xdddddddd${suffix.slice(2)}` as const, ) expect(parsed).toEqual({ ...input, id: 2 }) }) test('with service codes', () => { const suffix = Attribution.toDataSuffix({ appCode: 'baseapp', walletCode: 'privy', serviceCodes: ['flashbots', 'titan'], }) const parsed = Attribution.fromData(`0xdddddddd${suffix.slice(2)}`) expect(parsed).toEqual({ appCode: 'baseapp', walletCode: 'privy', serviceCodes: ['flashbots', 'titan'], id: 2, }) }) test('service codes only', () => { const suffix = Attribution.toDataSuffix({ serviceCodes: ['flashbots'], }) const parsed = Attribution.fromData(`0xdddddddd${suffix.slice(2)}`) expect(parsed).toEqual({ serviceCodes: ['flashbots'], id: 2, }) }) test('empty service codes array is omitted', () => { const suffix = Attribution.toDataSuffix({ appCode: 'baseapp', serviceCodes: [], }) const parsed = Attribution.fromData(`0xdddddddd${suffix.slice(2)}`) expect(parsed).toEqual({ appCode: 'baseapp', id: 2, }) }) test('wallet code only', () => { const suffix = Attribution.toDataSuffix({ walletCode: 'metamask', }) const parsed = Attribution.fromData(`0xdddddddd${suffix.slice(2)}`) expect(parsed).toEqual({ walletCode: 'metamask', id: 2, }) }) test('with both app and wallet registries', () => { const input: Attribution.AttributionSchemaId2 = { appCode: 'baseapp', walletCode: 'privy', registries: { app: { address: '0xBcf2B9598Ec0781eE49230CaACF0FB3C881B5195', chainId: 8453, }, wallet: { address: '0x1234567890123456789012345678901234567890', chainId: 1, }, }, } const suffix = Attribution.toDataSuffix(input) const parsed = Attribution.fromData( `0xdddddddd${suffix.slice(2)}` as const, ) expect(parsed).toEqual({ ...input, id: 2 }) }) test('empty metadata object is omitted', () => { const suffix = Attribution.toDataSuffix({ appCode: 'baseapp', metadata: {}, }) const parsed = Attribution.fromData(`0xdddddddd${suffix.slice(2)}`) // Empty metadata should not appear in decoded result expect(parsed).toEqual({ appCode: 'baseapp', id: 2, }) }) }) describe('fromData', () => { test('spec test: single entity', () => { const input = '0xdddddddda161616762617365617070000b0280218021802180218021802180218021' const result = Attribution.fromData(input) expect(result).toEqual({ appCode: 'baseapp', id: 2, }) }) test('spec test: multiple entities with metadata', () => { const input = '0xdddddddda46161676261736561707061776570726976796172a16161a26163663078323130356161782a307842636632423935393845633037383165453439323330436141434630464233433838314235313935616da26c75746d5f63616d706169676e6c77696e7465722d70726f6d6f66736f7572636566776562617070007b0280218021802180218021802180218021' const result = Attribution.fromData(input) expect(result).toEqual({ appCode: 'baseapp', walletCode: 'privy', registries: { app: { address: '0xBcf2B9598Ec0781eE49230CaACF0FB3C881B5195', chainId: 8453, }, }, metadata: { utm_campaign: 'winter-promo', source: 'webapp', }, id: 2, }) }) test('spec test: app + wallet + service codes', () => { const input = '0xdddddddda361616762617365617070617765707269767961738269666c617368626f747365746974616e00260280218021802180218021802180218021' const result = Attribution.fromData(input) expect(result).toEqual({ appCode: 'baseapp', walletCode: 'privy', serviceCodes: ['flashbots', 'titan'], id: 2, }) }) test.each([ [{ appCode: 'baseapp', id: 2 }], [{ walletCode: 'privy', id: 2 }], [{ appCode: 'baseapp', walletCode: 'privy', id: 2 }], [ { appCode: 'baseapp', metadata: { campaign: 'test', source: 'web' }, id: 2, }, ], [ { appCode: 'myapp', walletCode: 'mywallet', registries: { app: { address: '0xBcf2B9598Ec0781eE49230CaACF0FB3C881B5195' as const, chainId: 8453, }, }, id: 2, }, ], [{ serviceCodes: ['flashbots'], id: 2 }], [ { appCode: 'baseapp', walletCode: 'privy', serviceCodes: ['flashbots', 'titan'], id: 2, }, ], [ { appCode: 'baseapp', serviceCodes: ['builder0x69'], metadata: { source: 'webapp' }, id: 2, }, ], ] as Attribution.AttributionSchemaId2[][])( 'roundtrip %j', (attribution) => { const suffix = Attribution.toDataSuffix(attribution) const fullData = `0xabcdef${suffix.slice(2)}` as const const parsed = Attribution.fromData(fullData) expect(parsed).toEqual(attribution) }, ) }) })