diff --git a/src/renderer/shared/api/xcm/lib/__tests__/multi-location-service.test.ts b/src/renderer/shared/api/xcm/lib/__tests__/multi-location-service.test.ts new file mode 100644 index 0000000000..65b3324eb7 --- /dev/null +++ b/src/renderer/shared/api/xcm/lib/__tests__/multi-location-service.test.ts @@ -0,0 +1,200 @@ +import { + createAbsoluteMultiLocation, + createHere, + createParachainJunction, + createRelativeMultiLocation, +} from '../location-types'; +import { multiLocationService } from '../multi-location-service'; + +describe('shared/api/xcm/lib/multi-location-service', () => { + describe('reanchorAbsoluteLocation', () => { + test('reanchor global pov should remain unchanged', () => { + const initial = createAbsoluteMultiLocation(createParachainJunction(1000)); + const pov = createAbsoluteMultiLocation(...createHere()); + const expected = createRelativeMultiLocation(0, createParachainJunction(1000)); + + const result = multiLocationService.reanchorAbsoluteLocation(initial, pov); + + expect(result).toEqual(expected); + }); + + test('reanchor no common junctions', () => { + const initial = createAbsoluteMultiLocation(createParachainJunction(1000)); + const pov = createAbsoluteMultiLocation(createParachainJunction(2000)); + const expected = createRelativeMultiLocation(1, createParachainJunction(1000)); + + const result = multiLocationService.reanchorAbsoluteLocation(initial, pov); + + expect(result).toEqual(expected); + }); + + test('reanchor one common junction', () => { + const initial = createAbsoluteMultiLocation(createParachainJunction(1000), createParachainJunction(2000)); + const pov = createAbsoluteMultiLocation(createParachainJunction(1000), createParachainJunction(3000)); + const expected = createRelativeMultiLocation(1, createParachainJunction(2000)); + + const result = multiLocationService.reanchorAbsoluteLocation(initial, pov); + + expect(result).toEqual(expected); + }); + + test('reanchor all common junction', () => { + const initial = createAbsoluteMultiLocation(createParachainJunction(1000), createParachainJunction(2000)); + const pov = createAbsoluteMultiLocation(createParachainJunction(1000), createParachainJunction(2000)); + const expected = createRelativeMultiLocation(0); + + const result = multiLocationService.reanchorAbsoluteLocation(initial, pov); + + expect(result).toEqual(expected); + }); + + test('reanchor global to global', () => { + const initial = createAbsoluteMultiLocation(...createHere()); + const pov = createAbsoluteMultiLocation(...createHere()); + const expected = createRelativeMultiLocation(0); + + const result = multiLocationService.reanchorAbsoluteLocation(initial, pov); + + expect(result).toEqual(expected); + }); + + test('reanchor pov is successor of initial', () => { + const initial = createAbsoluteMultiLocation(...createHere()); + const pov = createAbsoluteMultiLocation(createParachainJunction(1000)); + const expected = createRelativeMultiLocation(1); + + const result = multiLocationService.reanchorAbsoluteLocation(initial, pov); + + expect(result).toEqual(expected); + }); + + test('reanchor initial is successor of pov', () => { + const initial = createAbsoluteMultiLocation(createParachainJunction(1000), createParachainJunction(2000)); + const pov = createAbsoluteMultiLocation(createParachainJunction(1000)); + const expected = createRelativeMultiLocation(0, createParachainJunction(2000)); + + const result = multiLocationService.reanchorAbsoluteLocation(initial, pov); + + expect(result).toEqual(expected); + }); + }); + + describe('restoreAbsoluteLocation', () => { + test('restore global pov should remain unchanged', () => { + const expected = createAbsoluteMultiLocation(createParachainJunction(1000)); + const pov = createAbsoluteMultiLocation(...createHere()); + + const relative = createRelativeMultiLocation(0, createParachainJunction(1000)); + + const restored = multiLocationService.restoreAbsoluteLocation(relative, pov); + expect(restored).toEqual(expected); + }); + + test('restore no common junctions', () => { + const expected = createAbsoluteMultiLocation(createParachainJunction(1000)); + const pov = createAbsoluteMultiLocation(createParachainJunction(2000)); + + const relative = createRelativeMultiLocation(1, createParachainJunction(1000)); + + const restored = multiLocationService.restoreAbsoluteLocation(relative, pov); + expect(restored).toEqual(expected); + }); + + test('restore one common junction', () => { + const expected = createAbsoluteMultiLocation(createParachainJunction(1000), createParachainJunction(2000)); + const pov = createAbsoluteMultiLocation(createParachainJunction(1000), createParachainJunction(3000)); + + const relative = createRelativeMultiLocation(1, createParachainJunction(2000)); + + const restored = multiLocationService.restoreAbsoluteLocation(relative, pov); + expect(restored).toEqual(expected); + }); + + test('restore all common junction', () => { + const expected = createAbsoluteMultiLocation(createParachainJunction(1000), createParachainJunction(2000)); + const pov = createAbsoluteMultiLocation(createParachainJunction(1000), createParachainJunction(2000)); + + const relative = createRelativeMultiLocation(0); + + const restored = multiLocationService.restoreAbsoluteLocation(relative, pov); + expect(restored).toEqual(expected); + }); + + test('restore global to global', () => { + const expected = createAbsoluteMultiLocation(...createHere()); + const pov = createAbsoluteMultiLocation(...createHere()); + + const relative = createRelativeMultiLocation(0); + + const restored = multiLocationService.restoreAbsoluteLocation(relative, pov); + expect(restored).toEqual(expected); + }); + + test('restore pov is successor of initial', () => { + const expected = createAbsoluteMultiLocation(...createHere()); + const pov = createAbsoluteMultiLocation(createParachainJunction(1000)); + + // Target is ancestor of POV: go up 1, then Here + const relative = createRelativeMultiLocation(1); + + const restored = multiLocationService.restoreAbsoluteLocation(relative, pov); + expect(restored).toEqual(expected); + }); + + test('restore initial is successor of pov', () => { + const expected = createAbsoluteMultiLocation(createParachainJunction(1000), createParachainJunction(2000)); + const pov = createAbsoluteMultiLocation(createParachainJunction(1000)); + + const relative = createRelativeMultiLocation(0, createParachainJunction(2000)); + + const restored = multiLocationService.restoreAbsoluteLocation(relative, pov); + expect(restored).toEqual(expected); + }); + + test('should throw error for negative parents', () => { + const pov = createAbsoluteMultiLocation(createParachainJunction(1000)); + const relative = createRelativeMultiLocation(-1, createParachainJunction(2000)); + + expect(() => { + multiLocationService.restoreAbsoluteLocation(relative, pov); + }).toThrow('Parents cannot be negative'); + }); + + test('should throw error when parents exceed pov junctions', () => { + const pov = createAbsoluteMultiLocation(createParachainJunction(1000)); + const relative = createRelativeMultiLocation(2, createParachainJunction(2000)); + + expect(() => { + multiLocationService.restoreAbsoluteLocation(relative, pov); + }).toThrow( + 'Invalid relative location from given pov: Relative location has 2 parents whereas pov has only 1 junctions', + ); + }); + + test('bidirectional test: reanchor then restore should return original', () => { + const original = createAbsoluteMultiLocation(createParachainJunction(1000), createParachainJunction(2000)); + const pov = createAbsoluteMultiLocation(createParachainJunction(1000), createParachainJunction(3000)); + + // Reanchor to relative + const relative = multiLocationService.reanchorAbsoluteLocation(original, pov); + + // Restore back to absolute + const restored = multiLocationService.restoreAbsoluteLocation(relative, pov); + + expect(restored).toEqual(original); + }); + + test('bidirectional test: restore then reanchor should return original relative', () => { + const relative = createRelativeMultiLocation(1, createParachainJunction(2000)); + const pov = createAbsoluteMultiLocation(createParachainJunction(1000), createParachainJunction(3000)); + + // Restore to absolute + const absolute = multiLocationService.restoreAbsoluteLocation(relative, pov); + + // Reanchor back to relative + const reanchored = multiLocationService.reanchorAbsoluteLocation(absolute, pov); + + expect(reanchored).toEqual(relative); + }); + }); +}); diff --git a/src/renderer/shared/api/xcm/lib/location-types.ts b/src/renderer/shared/api/xcm/lib/location-types.ts new file mode 100644 index 0000000000..670b0736f9 --- /dev/null +++ b/src/renderer/shared/api/xcm/lib/location-types.ts @@ -0,0 +1,46 @@ +import { type HexString } from '@/shared/core'; + +export type Junction = + | { Parachain: number } + | { AccountId32: { network?: string; id: HexString } } + | { AccountKey20: { network?: string; key: HexString } } + | { PalletInstance: number } + | { GeneralIndex: string } + | { GeneralKey: { length: number; data: HexString } }; + +export type RelativeMultiLocation = { + parents: number; + interior: Junction[]; +}; + +export type AbsoluteMultiLocation = { + interior: Junction[]; +}; + +export type XcmVersion = 2 | 3 | 4 | 5; + +export type VersionedXcm = { + xcm: T; + version: XcmVersion; +}; + +// Helper functions for creating multi-locations +export function createParachainJunction(id: number): Junction { + return { Parachain: id }; +} + +export function createAbsoluteMultiLocation(...junctions: Junction[]): AbsoluteMultiLocation { + return { interior: junctions }; +} + +export function createRelativeMultiLocation(parents: number, ...junctions: Junction[]): RelativeMultiLocation { + return { parents, interior: junctions }; +} + +export function createHere(): Junction[] { + return []; +} + +export function createVersionedXcm(xcm: T, version: XcmVersion): VersionedXcm { + return { xcm, version }; +} diff --git a/src/renderer/shared/api/xcm/lib/multi-location-service.ts b/src/renderer/shared/api/xcm/lib/multi-location-service.ts new file mode 100644 index 0000000000..e5d53694a9 --- /dev/null +++ b/src/renderer/shared/api/xcm/lib/multi-location-service.ts @@ -0,0 +1,68 @@ +import { type AbsoluteMultiLocation, type Junction, type RelativeMultiLocation } from './location-types'; + +function areJunctionsEqual(junction1: Junction, junction2: Junction): boolean { + return JSON.stringify(junction1) === JSON.stringify(junction2); +} + +function findLastCommonJunctionIndex( + location1: AbsoluteMultiLocation, + location2: AbsoluteMultiLocation, +): number | null { + let lastCommonIndex = -1; + + const minLength = Math.min(location1.interior.length, location2.interior.length); + + for (let i = 0; i < minLength; i++) { + if (areJunctionsEqual(location1.interior[i], location2.interior[i])) { + lastCommonIndex = i; + } else { + break; + } + } + + return lastCommonIndex >= 0 ? lastCommonIndex : null; +} + +function reanchorAbsoluteLocation( + location: AbsoluteMultiLocation, + pointOfView: AbsoluteMultiLocation, +): RelativeMultiLocation { + const lastCommonIndex = findLastCommonJunctionIndex(location, pointOfView); + const firstDistinctIndex = lastCommonIndex !== null ? lastCommonIndex + 1 : 0; + + const parents = pointOfView.interior.length - firstDistinctIndex; + const interior = location.interior.slice(firstDistinctIndex); + + return { + parents, + interior, + }; +} + +function restoreAbsoluteLocation( + relativeLocation: RelativeMultiLocation, + pointOfView: AbsoluteMultiLocation, +): AbsoluteMultiLocation { + if (relativeLocation.parents < 0) { + throw new Error('Parents cannot be negative'); + } + + if (relativeLocation.parents > pointOfView.interior.length) { + throw new Error( + `Invalid relative location from given pov: ` + + `Relative location has ${relativeLocation.parents} parents whereas pov has only ${pointOfView.interior.length} junctions`, + ); + } + + const base = pointOfView.interior.slice(0, pointOfView.interior.length - relativeLocation.parents); + const resultJunctions = [...base, ...relativeLocation.interior]; + + return { + interior: resultJunctions, + }; +} + +export const multiLocationService = { + reanchorAbsoluteLocation, + restoreAbsoluteLocation, +};