Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create registry class helper #22

Merged
merged 1 commit into from
Apr 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 } 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, id => Metadata.fromJson(id));
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
Loading