Skip to content

Commit

Permalink
Create registry class helper
Browse files Browse the repository at this point in the history
  • Loading branch information
grod220 committed Apr 24, 2024
1 parent 2b40844 commit 325cbc8
Show file tree
Hide file tree
Showing 10 changed files with 490 additions and 247 deletions.
6 changes: 6 additions & 0 deletions npm/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# @penumbra-labs/registry

## 5.0.0

### Major Changes

- New registry class

## 4.1.0

### Minor Changes
Expand Down
11 changes: 6 additions & 5 deletions npm/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@penumbra-labs/registry",
"version": "4.1.0",
"version": "5.0.0",
"description": "Chain and asset registry for Penumbra",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
Expand All @@ -15,15 +15,16 @@
"test": "vitest run"
},
"devDependencies": {
"@buf/penumbra-zone_penumbra.bufbuild_es": "1.8.0-20240415223544-c0a709144747.2",
"@buf/penumbra-zone_penumbra.bufbuild_es": "1.9.0-20240423164216-32b3675faa56.1",
"@bufbuild/protobuf": "^1.9.0",
"@changesets/cli": "^2.27.1",
"@eslint/eslintrc": "^3.0.2",
"@eslint/js": "^9.0.0",
"eslint": "^9.0.0",
"@eslint/js": "^9.1.1",
"eslint": "^9.1.1",
"prettier": "^3.2.5",
"tsup": "^8.0.2",
"typescript": "^5.4.5",
"typescript-eslint": "^7.7.0",
"typescript-eslint": "^7.7.1",
"vitest": "^1.5.0"
},
"files": [
Expand Down
379 changes: 195 additions & 184 deletions npm/pnpm-lock.yaml

Large diffs are not rendered by default.

37 changes: 1 addition & 36 deletions npm/src/client.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,6 @@
import { GithubFetcher } from './github';
import {
AssetId,
Metadata,
} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb';
import { deriveTestnetChainIdFromPreview, isTestnetPreviewChainId } from './utils';

// @ts-expect-error alias for dev only
// eslint-disable-next-line @typescript-eslint/no-unused-vars
type Jsonified<T> = string;

export interface Registry {
chainId: string;
ibcConnections: Chain[];
rpcs: Rpc[];
assetById: Record<Jsonified<AssetId>, Jsonified<Metadata>>;
stakingAssetId: Jsonified<AssetId>;
numeraires: Jsonified<AssetId>[];
}

export interface Chain {
addressPrefix: string;
chainId: string;
ibcChannel: string;
images: Image[];
displayName: string;
}

export interface Rpc {
name: string;
url: string;
images: Image[];
}

export interface Image {
png?: string;
svg?: string;
}
import { Registry } from './registry';

export class ChainRegistryClient {
private readonly github: GithubFetcher;
Expand Down
31 changes: 14 additions & 17 deletions npm/src/github.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,14 @@
import { Registry } from './client';
import { Metadata } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb';
import { Jsonified } from './utils';
import { Base64AssetId, Chain, Registry, Rpc } from './registry';

export interface GithubContentsRes {
name: string;
path: string;
sha: string;
size: number;
url: string;
html_url: string;
git_url: string;
download_url: string;
type: string;
_links: {
self: string;
git: string;
html: string;
};
export interface GithubRegistryResponse {
chainId: string;
ibcConnections: Chain[];
rpcs: Rpc[];
assetById: Record<Base64AssetId, Jsonified<Metadata>>;
stakingAssetId: Base64AssetId;
numeraires: Base64AssetId[];
}

const REGISTRY_BASE_URL = 'https://raw.githubusercontent.com/prax-wallet/registry/main/registry';
Expand All @@ -26,7 +20,10 @@ export class GithubFetcher {

async fetchRegistryData(chainId: ChainId): Promise<Registry> {
if (this.cache[chainId]) return this.cache[chainId]!;
return this.typedFetcher<Registry>(`${REGISTRY_BASE_URL}/${chainId}.json`);
const response = await this.typedFetcher<GithubRegistryResponse>(
`${REGISTRY_BASE_URL}/${chainId}.json`,
);
return new Registry(response);
}

clearCache(): void {
Expand Down
1 change: 1 addition & 0 deletions npm/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './client';
export * from './registry';
101 changes: 101 additions & 0 deletions npm/src/registry.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { describe, expect, it } from 'vitest';
import { base64ToUint8Array, isTestnetPreviewChainId } from './utils';
import { Registry } from './registry';
import { GithubRegistryResponse } from './github';
import { AssetId } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb';

const testRegistry: GithubRegistryResponse = {
chainId: 'penumbra-testnet-deimos-6',
ibcConnections: [
{
addressPrefix: 'osmo',
chainId: 'osmo-test-5',
ibcChannel: 'channel-3',
displayName: 'Osmosis',
images: [
{
svg: 'https://raw.githubusercontent.com/cosmos/chain-registry/f1348793beb994c6cc0256ed7ebdb48c7aa70003/osmosis/images/osmo.svg',
},
],
},
],
rpcs: [
{
name: 'Penumbra Labs Testnet RPC',
url: 'https://grpc.testnet.penumbra.zone',
images: [
{
png: 'https://raw.githubusercontent.com/prax-wallet/registry/main/images/penumbra-favicon.png',
},
],
},
],
assetById: {
'KeqcLzNx9qSH5+lcJHBB9KNW+YPrBk5dKzvPMiypahA=': {
denomUnits: [
{
denom: 'penumbra',
exponent: 6,
},
{
denom: 'mpenumbra',
exponent: 3,
},
{
denom: 'upenumbra',
},
],
base: 'upenumbra',
display: 'penumbra',
symbol: 'UM',
penumbraAssetId: {
inner: 'KeqcLzNx9qSH5+lcJHBB9KNW+YPrBk5dKzvPMiypahA=',
},
images: [
{
svg: 'https://raw.githubusercontent.com/prax-wallet/registry/main/images/um.svg',
},
],
},
'reum7wQmk/owgvGMWMZn/6RFPV24zIKq3W6In/WwZgg=': {
denomUnits: [
{
denom: 'test_usd',
exponent: 18,
},
{
denom: 'wtest_usd',
},
],
base: 'wtest_usd',
display: 'test_usd',
symbol: 'TestUSD',
penumbraAssetId: {
inner: 'reum7wQmk/owgvGMWMZn/6RFPV24zIKq3W6In/WwZgg=',
},
images: [
{
svg: 'https://raw.githubusercontent.com/prax-wallet/registry/main/images/test-usd.svg',
},
],
},
},
stakingAssetId: 'KeqcLzNx9qSH5+lcJHBB9KNW+YPrBk5dKzvPMiypahA=',
numeraires: ['reum7wQmk/owgvGMWMZn/6RFPV24zIKq3W6In/WwZgg='],
};

describe('Registry', () => {
it('get metadata successfully', () => {
const registry = new Registry(testRegistry);
const usdcId = base64ToUint8Array('reum7wQmk/owgvGMWMZn/6RFPV24zIKq3W6In/WwZgg=');
const res = registry.getMetadata(new AssetId({ inner: usdcId }));
expect(res.base).toEqual('wtest_usd');
});

it('throw when searching for metadata that does not exist', () => {
const registry = new Registry(testRegistry);
const cubeId = base64ToUint8Array('6KBVsPINa8gWSHhfH+kAFJC4afEJA3EtuB2HyCqJUws=');
const getCubeMetadata = () => registry.getMetadata(new AssetId({ inner: cubeId }));
expect(getCubeMetadata).toThrow();
});
});
55 changes: 55 additions & 0 deletions npm/src/registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import {
AssetId,
Metadata,
} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb';
import { base64ToUint8Array, mapObjectValues, Stringified, uint8ArrayToBase64 } from './utils';
import { GithubRegistryResponse } from './github';

export type Base64AssetId = Stringified<AssetId['inner']>;

export interface Chain {
addressPrefix: string;
chainId: string;
ibcChannel: string;
images: Image[];
displayName: string;
}

export interface Rpc {
name: string;
url: string;
images: Image[];
}

export interface Image {
png?: string;
svg?: string;
}

export class Registry {
public readonly chainId: string;
public readonly ibcConnections: Chain[];
public readonly rpcs: Rpc[];
public readonly stakingAssetId: AssetId;
public readonly numeraires: AssetId[];

private readonly assetById: Record<Base64AssetId, Metadata>;

constructor(response: GithubRegistryResponse) {
this.chainId = response.chainId;
this.ibcConnections = response.ibcConnections;
this.rpcs = response.rpcs;
this.assetById = mapObjectValues(response.assetById, Metadata.fromJson);
this.stakingAssetId = new AssetId({ inner: base64ToUint8Array(response.stakingAssetId) });
this.numeraires = response.numeraires.map(a => new AssetId({ inner: base64ToUint8Array(a) }));
}

getMetadata(id: AssetId): Metadata {
const key = uint8ArrayToBase64(id.inner);
const metadata = this.assetById[key];
if (!metadata) {
throw new Error(`No metadata in registry for asset id: ${key}`);
}
return metadata;
}
}
80 changes: 75 additions & 5 deletions npm/src/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
import { describe, expect, test } from 'vitest';
import { deriveTestnetChainIdFromPreview, isTestnetPreviewChainId } from './utils';
import { describe, expect, it } from 'vitest';
import {
base64ToUint8Array,
deriveTestnetChainIdFromPreview,
isTestnetPreviewChainId,
mapObjectValues,
uint8ArrayToBase64,
} from './utils';

describe('testnet-preview helper', () => {
test('should correctly identify testnet-preview chain-id', () => {
it('should correctly identify testnet-preview chain-id', () => {
expect(isTestnetPreviewChainId('penumbra-testnet-deimos-6-711be12a')).toBeTruthy();
expect(isTestnetPreviewChainId('penumbra-testnet-deimos-222-711be12a')).toBeTruthy();
expect(isTestnetPreviewChainId('penumbra-testnet-rhea-8b2dfc5c')).toBeTruthy();
expect(isTestnetPreviewChainId('penumbra-testnet-tethy12-8777cb20')).toBeTruthy();
});

test('should not identify chain-id as testnet-preview', () => {
it('should not identify chain-id as testnet-preview', () => {
expect(isTestnetPreviewChainId('penumbra-mainnet')).toBeFalsy();
expect(isTestnetPreviewChainId('penumbra-testnet-rhea')).toBeFalsy();
expect(isTestnetPreviewChainId('penumbra-testnet-deimos-6')).toBeFalsy();
});

test('should correctly derive testnet chain-id from testnet-preview chain-id', () => {
it('should correctly derive testnet chain-id from testnet-preview chain-id', () => {
expect(deriveTestnetChainIdFromPreview('penumbra-testnet-deimos-6-711be12a')).toEqual(
'penumbra-testnet-deimos-6',
);
Expand All @@ -27,3 +33,67 @@ describe('testnet-preview helper', () => {
);
});
});

describe('mapObjectValues', () => {
it('should apply a function to each value in the object', () => {
const original = { a: 1, b: 2, c: 3 };
const expected = { a: 2, b: 3, c: 4 };
const result = mapObjectValues(original, x => x + 1);
expect(result).toEqual(expected);
});

it('should handle objects with various types of values', () => {
const original = { a: 'hello', b: true, c: 10 };
const expected = { a: 'HELLO', b: 'TRUE', c: '10' };
const result = mapObjectValues(original, x => String(x).toUpperCase());
expect(result).toEqual(expected);
});

it('should not mutate the original object', () => {
const original = { a: 1, b: 2, c: 3 };
const originalCopy = { ...original };
mapObjectValues(original, x => x * 10);
expect(original).toEqual(originalCopy);
});
});

describe('uint8ArrayToBase64', () => {
it('converts an empty Uint8Array to an empty string', () => {
const byteArray = new Uint8Array();
const result = uint8ArrayToBase64(byteArray);
expect(result).toBe('');
});

it('correctly converts a Uint8Array to a base64 string', () => {
const byteArray = new Uint8Array([104, 101, 108, 108, 111]); // ASCII for "hello"
const result = uint8ArrayToBase64(byteArray);
expect(result).toBe('aGVsbG8='); // base64 for "hello"
});

it('correctly converts a large Uint8Array to a base64 string', () => {
const largeArray = new Uint8Array(1024).fill(97); // 1024 bytes all set to ASCII for "a"
const result = uint8ArrayToBase64(largeArray);
expect(result).toBe(Buffer.from(largeArray).toString('base64')); // compare with Node's built-in conversion
});
});

describe('base64ToUint8Array', () => {
it('should correctly convert a Base64 string to Uint8Array', () => {
const base64String = 'SGVsbG8gd29ybGQ='; // 'Hello world' in Base64
const expectedUint8Array = new Uint8Array([
72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100,
]); // 'Hello world' in ASCII values
expect(base64ToUint8Array(base64String)).toEqual(expectedUint8Array);
});

it('should return an empty Uint8Array for an empty Base64 string', () => {
const base64String = '';
const expectedUint8Array = new Uint8Array([]);
expect(base64ToUint8Array(base64String)).toEqual(expectedUint8Array);
});

it('should handle non-Base64 strings', () => {
const nonBase64String = 'This is not a Base64 string';
expect(() => base64ToUint8Array(nonBase64String)).toThrow();
});
});
Loading

0 comments on commit 325cbc8

Please sign in to comment.