Skip to content
Open
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
25 changes: 25 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions wallets/zunia-extension/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Change Log

All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.

# 0.11.0

### Features

- add the Zunia browser extension adapter (`window.zunia`)
47 changes: 47 additions & 0 deletions wallets/zunia-extension/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# @interchain-kit/zunia-extension

Zunia browser extension adapter for [Interchain Kit](https://github.com/hyperweb-io/interchain-kit).

Zunia is a non-custodial Cosmos wallet. This package connects a dApp to `window.zunia`. On a phone browser, where the extension is not injected, it opens the Zunia app over WalletConnect.

## Install

```sh
npm install @interchain-kit/zunia-extension @interchain-kit/react
```

## Usage

```tsx
import { ChainProvider } from '@interchain-kit/react';
import { zuniaWallet } from '@interchain-kit/zunia-extension';
import { assetList as cosmoshubAssetList, chain as cosmoshubChain } from 'chain-registry/mainnet/cosmoshub';

export function App() {
return (
<ChainProvider chains={[cosmoshubChain]} assetLists={[cosmoshubAssetList]} wallets={[zuniaWallet]}>
{/* your app */}
</ChainProvider>
);
}
```

Use the same chain id the extension has. Pass every chain the dApp should connect to in `chains`.

## What it calls

| | |
|---|---|
| Window | `window.zunia` |
| Account change | `zunia_keystorechange` |
| Connect | `enable`, then `experimentalSuggestChain` when the chain is unknown, then `enable` again |
| Sign | `signAmino`, `signDirect`, `signArbitrary` |
| Broadcast | The dApp broadcasts. `sendTx` is refused. |

Chrome 0.1.2: [Chrome Web Store](https://chromewebstore.google.com/detail/zunia/ngokakoekdogobjmokipglbcclelgajk). Install page: [zunialab.com](https://zunialab.com).

There is no Ethereum provider. Do not set an `eip155` chain on this wallet.

## Phone browser

The mobile link is `zunia://wc?uri=<wc-uri>` (Android intent package `com.zuniawallet.zunia_mobile`). That is the URI the Zunia app pairs. For a desktop or in-app WalletConnect session, use `@interchain-kit/zunia-mobile`.
77 changes: 77 additions & 0 deletions wallets/zunia-extension/__tests__/zunia.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { zuniaWallet } from '../src';
import { ZuniaCosmosWallet } from '../src/cosmos';
import { ZUNIA_ANDROID_PACKAGE, ZUNIA_WC_LINK } from '../src/registry';

const WC_URI = 'wc:abc123@2?relay-protocol=irn&symKey=ff';

function uriParam(link: string): string | null {
const withoutHash = link.split('#')[0];
const query = withoutHash.slice(withoutHash.indexOf('?') + 1);
return new URLSearchParams(query).get('uri');
}

describe('Zunia extension', () => {
it('talks to window.zunia and not to an Ethereum provider', () => {
expect(zuniaWallet.info.name).toBe('zunia-extension');
expect(zuniaWallet.info.prettyName).toBe('Zunia');
expect(zuniaWallet.info.mode).toBe('extension');
expect(zuniaWallet.info.windowKey).toBe('zunia');
expect(zuniaWallet.info.cosmosKey).toBe('zunia');
expect(zuniaWallet.info.keystoreChange).toBe('zunia_keystorechange');
expect(zuniaWallet.info.ethereumKey).toBeUndefined();
expect(zuniaWallet.info.downloads).toEqual(
expect.arrayContaining([
expect.objectContaining({
browser: 'chrome',
link: 'https://chromewebstore.google.com/detail/zunia/ngokakoekdogobjmokipglbcclelgajk',
}),
expect.objectContaining({
link: 'https://zunialab.com',
}),
])
);
});

it('opens the phone with the uri query the app reads', () => {
const encoded = encodeURIComponent(WC_URI);
const ios = ZUNIA_WC_LINK.ios.replace('{wc-uri}', encoded);
const android = ZUNIA_WC_LINK.android.replace('{wc-uri}', encoded);

expect(ios.startsWith('zunia://wc?uri=')).toBe(true);
expect(uriParam(ios)).toBe(WC_URI);
expect(android).toContain(`package=${ZUNIA_ANDROID_PACKAGE}`);
expect(android).toContain('scheme=zunia');
expect(uriParam(android)).toBe(WC_URI);
});

it('asks again to connect after a chain suggestion', async () => {
const wallet = new ZuniaCosmosWallet(zuniaWallet.info);
const enable = jest
.fn()
.mockRejectedValueOnce(new Error('Zunia does not know the chain osmosis-1. Add it with experimentalSuggestChain first.'))
.mockResolvedValueOnce(undefined);
wallet.client = { enable };
jest.spyOn(wallet, 'addSuggestChain').mockResolvedValue(undefined);

await wallet.connect('osmosis-1');

expect(wallet.addSuggestChain).toHaveBeenCalledWith('osmosis-1');
expect(enable).toHaveBeenCalledTimes(2);
});

it('leaves a rejection alone', async () => {
const wallet = new ZuniaCosmosWallet(zuniaWallet.info);
wallet.client = { enable: jest.fn().mockRejectedValue(new Error('Request rejected')) };
const suggest = jest.spyOn(wallet, 'addSuggestChain').mockResolvedValue(undefined);

await expect(wallet.connect('cosmoshub-4')).rejects.toThrow('Request rejected');
expect(suggest).not.toHaveBeenCalled();
});

it('does not broadcast a dApp transaction', async () => {
const wallet = new ZuniaCosmosWallet(zuniaWallet.info);
await expect(wallet.sendTx('cosmoshub-4', new Uint8Array(), 'sync' as never)).rejects.toThrow(
'does not broadcast'
);
});
});
21 changes: 21 additions & 0 deletions wallets/zunia-extension/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
transform: {
'^.+\\.tsx?$': [
'ts-jest',
{
babelConfig: false,
tsconfig: 'tsconfig.test.json',
},
],
},
transformIgnorePatterns: [`/node_modules/*`],
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$',
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
modulePathIgnorePatterns: ['dist/*'],
moduleNameMapper: {
'^@interchain-kit/core$': '<rootDir>/../../packages/core/src',
},
};
44 changes: 44 additions & 0 deletions wallets/zunia-extension/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"name": "@interchain-kit/zunia-extension",
"version": "0.11.0",
"author": "Zunia Lab <dev@zunialab.com>",
"description": "Zunia browser extension wallet adapter for Interchain Kit",
"main": "index.js",
"module": "esm/index.js",
"types": "index.d.ts",
"homepage": "https://github.com/hyperweb-io/interchain-kit",
"license": "MIT",
"publishConfig": {
"access": "public",
"directory": "dist"
},
"repository": {
"type": "git",
"url": "https://github.com/hyperweb-io/interchain-kit"
},
"bugs": {
"url": "https://github.com/hyperweb-io/interchain-kit/issues"
},
"scripts": {
"clean": "makage clean",
"build": "makage build",
"build:dev": "npm run clean; tsc --declarationMap; tsc -p tsconfig.esm.json; npm run copy",
"lint": "eslint . --fix",
"test": "jest",
"test:watch": "jest --watch",
"watch:dev": "tsc -w -p tsconfig.esm.json & tsc -w",
"prepack": "npm run build"
},
"keywords": [
"zunia",
"cosmos",
"wallet",
"interchain-kit"
],
"dependencies": {
"@interchain-kit/core": "workspace:*"
},
"devDependencies": {
"makage": "^0.1.12"
}
}
3 changes: 3 additions & 0 deletions wallets/zunia-extension/src/constant.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Zunia mark, red tile. Same drawing as zunia-brand/svg/icon/zunia-icon-red-tile.svg, without embedded metadata.
export const ICON =
'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIj48cmVjdCB3aWR0aD0iNTEyIiBoZWlnaHQ9IjUxMiIgcng9IjEyMCIgZmlsbD0iI0ZGMUIwQyIvPjxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDk5LjEgNTkuOCkgc2NhbGUoMy4yNjkyKSI+PGRlZnM+PG1hc2sgaWQ9Im1nIiBtYXNrVW5pdHM9InVzZXJTcGFjZU9uVXNlIiB4PSIwIiB5PSIwIiB3aWR0aD0iOTYiIGhlaWdodD0iMTIwIj48cmVjdCB3aWR0aD0iOTYiIGhlaWdodD0iMTIwIiBmaWxsPSIjZmZmIi8+PHBhdGggZD0iTTI2IDIwIDcwIDQ2IDI2IDcyIiBmaWxsPSJub25lIiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMzAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjwvbWFzaz48L2RlZnM+PGcgbWFzaz0idXJsKCNtZykiPjxwYXRoIGQ9Ik0yNiA0OCA3MCA3NCAyNiAxMDAiIGZpbGw9Im5vbmUiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIyNCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PC9nPjxwYXRoIGQ9Ik0yNiAyMCA3MCA0NiAyNiA3MiIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjI0IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48L2c+PC9zdmc+';
34 changes: 34 additions & 0 deletions wallets/zunia-extension/src/cosmos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { BroadcastMode, CosmosWallet } from '@interchain-kit/core';

function errorMessage(error: unknown): string {
if (error instanceof Error) return error.message;
if (typeof error === 'object' && error && 'message' in error) {
return String((error as { message: unknown }).message);
}
return '';
}

/**
* Zunia's `experimentalSuggestChain` saves the chain and does not grant the site.
* The stock connector returns after the suggestion, so the next `getKey` is unauthorized.
* Connect again once the chain is known.
*
* `sendTx` is refused in the extension. The dApp signs, then broadcasts.
*/
export class ZuniaCosmosWallet extends CosmosWallet {
async connect(chainId: string): Promise<void> {
try {
await this.client.enable(chainId);
} catch (error) {
if (errorMessage(error).includes('rejected')) throw error;
await this.addSuggestChain(chainId);
await this.client.enable(chainId);
}
}

async sendTx(_chainId: string, _tx: Uint8Array, _mode: BroadcastMode): Promise<Uint8Array> {
throw new Error(
'Zunia does not broadcast dApp transactions. Sign with signAmino or signDirect and broadcast from the dApp.'
);
}
}
20 changes: 20 additions & 0 deletions wallets/zunia-extension/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { ExtensionWallet, selectWalletByPlatform, WCMobileWebWallet } from '@interchain-kit/core';

import { ZuniaCosmosWallet } from './cosmos';
import { zuniaExtensionInfo } from './registry';

export * from './registry';

const web = new ExtensionWallet(zuniaExtensionInfo);
web.setNetworkWallet('cosmos', new ZuniaCosmosWallet(zuniaExtensionInfo));

const zuniaWallet = selectWalletByPlatform(
{
mobileBrowser: new WCMobileWebWallet(zuniaExtensionInfo),
inAppBrowser: web,
desktopBrowser: web,
},
zuniaExtensionInfo
);

export { zuniaWallet };
42 changes: 42 additions & 0 deletions wallets/zunia-extension/src/registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Wallet } from '@interchain-kit/core';

import { ICON } from './constant';

/**
* Android application id and the custom scheme the Zunia app registers.
* The app only treats a link as WalletConnect when the `uri` query is a `wc:` URI.
* `zunia://wcV2?<wc-uri>` is ignored.
*/
export const ZUNIA_ANDROID_PACKAGE = 'com.zuniawallet.zunia_mobile';

export const ZUNIA_WC_LINK = {
android: `intent://wc?uri={wc-uri}#Intent;package=${ZUNIA_ANDROID_PACKAGE};scheme=zunia;end;`,
ios: 'zunia://wc?uri={wc-uri}',
};

export const zuniaExtensionInfo: Wallet = {
name: 'zunia-extension',
prettyName: 'Zunia',
logo: ICON,
mode: 'extension',
windowKey: 'zunia',
cosmosKey: 'zunia',
keystoreChange: 'zunia_keystorechange',
description: 'Non-custodial Cosmos wallet. One key, every chain.',
downloads: [
{
device: 'desktop',
browser: 'chrome',
link: 'https://chromewebstore.google.com/detail/zunia/ngokakoekdogobjmokipglbcclelgajk',
},
{
device: 'desktop',
browser: 'firefox',
link: 'https://addons.mozilla.org/en-US/firefox/addon/zunia-wallet/',
},
{
link: 'https://zunialab.com',
},
],
walletConnectLink: ZUNIA_WC_LINK,
};
9 changes: 9 additions & 0 deletions wallets/zunia-extension/tsconfig.esm.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "dist/esm",
"module": "es2022",
"rootDir": "src/",
"declaration": false
}
}
14 changes: 14 additions & 0 deletions wallets/zunia-extension/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src/",
"baseUrl": ".",
"paths": {
"@interchain-kit/core": ["../../packages/core/dist"],
"@interchain-kit/core/*": ["../../packages/core/dist/*"]
}
},
"include": ["src/**/*.ts"],
"exclude": ["dist", "node_modules", "**/*.spec.*", "**/*.test.*"]
}
11 changes: 11 additions & 0 deletions wallets/zunia-extension/tsconfig.test.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"paths": {
"@interchain-kit/core": ["../../packages/core/src"],
"@interchain-kit/core/*": ["../../packages/core/src/*"]
}
},
"include": ["src/**/*.ts", "__tests__/**/*.ts"],
"exclude": ["dist", "node_modules"]
}
10 changes: 10 additions & 0 deletions wallets/zunia-mobile/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Change Log

All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.

# 0.11.0

### Features

- add the Zunia WalletConnect adapter
Loading