diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..bf74463 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,9 @@ +## Task title + +> Description + +✨ Features: +- [ ] + +🔨 Improvements: +- [ ] \ No newline at end of file diff --git a/src/ada/available-wallets.ts b/src/ada/available-wallets.ts new file mode 100644 index 0000000..283e492 --- /dev/null +++ b/src/ada/available-wallets.ts @@ -0,0 +1,8 @@ +// TODO: Add klever mobile app source name +export const availableWallets = ['nami'] as const + +export type AvailableWallet = typeof availableWallets[number] + +export enum CardanoWallet { + NAMI = 'nami', +} diff --git a/src/ada/connect.ts b/src/ada/connect.ts new file mode 100644 index 0000000..de58df6 --- /dev/null +++ b/src/ada/connect.ts @@ -0,0 +1,30 @@ +import { NotInjectedError } from '@/errors'; +import { NoAvailableAccountsError } from '@/errors/no-accounts-available-error'; +import { NoProviderAvailableError } from '@/errors/no-provider-available-error'; +import { web3Window } from '@/types'; +import { availableWallets } from './available-wallets'; +import type { CardanoUsedAddress } from './types'; + +export async function connect(wallet?: string): Promise { + if (!web3Window.cardano) + throw new NotInjectedError() + + let injectedWallet = wallet + if (typeof injectedWallet === 'undefined') { + for (const availableWallet of availableWallets) { + if (web3Window.cardano[availableWallet]) + injectedWallet = availableWallet + } + } + + if (typeof injectedWallet === 'undefined') + throw new NoProviderAvailableError() + + await web3Window.cardano[injectedWallet].enable() + + const usedAddresses: CardanoUsedAddress[] = await web3Window.cardano.getUsedAddresses() + if (usedAddresses.length === 0) + throw new NoAvailableAccountsError() + + return usedAddresses +} diff --git a/src/ada/index.ts b/src/ada/index.ts new file mode 100644 index 0000000..ca37939 --- /dev/null +++ b/src/ada/index.ts @@ -0,0 +1,35 @@ +import type { ProviderEntity } from '@/entities/provider-entity'; +import type { Account, Address, Balance } from '@/types'; +import { CardanoWallet } from './available-wallets'; +import { connect } from './connect'; +import type { CardanoProviderProps } from './types'; + +export class CardanoProvider implements ProviderEntity { + wallet?: CardanoWallet + + constructor({ wallet }: CardanoProviderProps) { + if (wallet) + this.wallet = CardanoWallet[wallet.toUpperCase() as keyof typeof CardanoWallet] + } + + async connect(): Promise { + const accounts = await connect(this.wallet) + + return accounts.map((account, index) => ({ + name: `Account #${index}`, + address: account, + })) + } + + async getBalance(address: Address): Promise { + throw new Error('Not yet implemented.') + } + + async signMessage(address: Address, message: string): Promise { + throw new Error('Not yet implemented.') + } + + signatureVerify(message: string, signature: string, address: string): boolean { + throw new Error('Not yet implemented.') + } +} diff --git a/src/ada/types.ts b/src/ada/types.ts new file mode 100644 index 0000000..dc7ed8b --- /dev/null +++ b/src/ada/types.ts @@ -0,0 +1,7 @@ +import type { AvailableWallet } from './available-wallets'; + +export interface CardanoProviderProps { + wallet?: AvailableWallet +} + +export type CardanoUsedAddress = string diff --git a/src/index.ts b/src/index.ts index 9f7cfbf..8fa2d24 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,4 @@ +export * from './ada/index' export * from './networks' export * from './substrate/dot/index' export * from './substrate/ksm/index' diff --git a/src/networks.ts b/src/networks.ts index c6fd742..c67e67c 100644 --- a/src/networks.ts +++ b/src/networks.ts @@ -11,11 +11,17 @@ export const Networks = { name: 'Kusama', decimals: 12, }, + ada: { + id: 3, + name: 'Cardano', + decimals: 15, // TODO: Check this value + }, } export enum Network { POLKADOT = 'dot', KUSAMA = 'ksm', + CARDANO = 'ada', } export type NetworkKey = keyof typeof Networks diff --git a/src/substrate/connect.spec.ts b/src/substrate/connect.spec.ts index 8194982..3753a89 100644 --- a/src/substrate/connect.spec.ts +++ b/src/substrate/connect.spec.ts @@ -1,6 +1,7 @@ import { NotInjectedError } from '@/errors'; import { NoAvailableAccountsError } from '@/errors/no-accounts-available-error'; import { NoProviderAvailableError } from '@/errors/no-provider-available-error'; +import { web3Window } from '@/types'; import * as extension from '@polkadot/extension-dapp'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { connect } from './connect'; @@ -17,11 +18,11 @@ describe('Connect wallet use case', () => { vi.resetAllMocks() appName = 'Web3 Hub'; - (window as any).injectedWeb3 = {} + web3Window.injectedWeb3 = {} }) it('should be able to throw error when window dont have web3 object', async () => { - delete (window as any).injectedWeb3 + delete web3Window.injectedWeb3 await expect(connect(appName)).rejects.toThrow(NotInjectedError) }) diff --git a/src/substrate/connect.ts b/src/substrate/connect.ts index 9103197..660d749 100644 --- a/src/substrate/connect.ts +++ b/src/substrate/connect.ts @@ -1,11 +1,12 @@ import { NotInjectedError } from '@/errors'; import { NoAvailableAccountsError } from '@/errors/no-accounts-available-error'; import { NoProviderAvailableError } from '@/errors/no-provider-available-error'; +import { web3Window } from '@/types'; import { web3Accounts, web3Enable } from '@polkadot/extension-dapp'; import type { SubstrateAccountWithMeta } from './types'; export async function connect(appName: string): Promise { - if (!(window as any).injectedWeb3) + if (!web3Window.injectedWeb3) throw new NotInjectedError() const isInjected = await web3Enable(appName) diff --git a/src/types.ts b/src/types.ts index cf58b49..f612473 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,3 +1,5 @@ +import type { CardanoProviderProps } from './ada/types' +import type { NetworkKey } from './networks' import type { SubstrateProviderProps } from './substrate/types' export type Address = string @@ -12,4 +14,11 @@ export interface Balance { frozen: number | string } -export type ProviderBuilderProps = SubstrateProviderProps +export type ProviderBuilderProps = T extends 'dot' | 'ksm' ? SubstrateProviderProps : T extends 'ada' ? CardanoProviderProps : never; + +export type Web3Window = { + injectedWeb3: any + cardano: any +} & Window & typeof globalThis + +export const web3Window = (window as Web3Window) diff --git a/src/web3-provider.ts b/src/web3-provider.ts index fda3151..7ea3a8a 100644 --- a/src/web3-provider.ts +++ b/src/web3-provider.ts @@ -1,9 +1,12 @@ +import { CardanoProvider } from './ada'; +import type { CardanoProviderProps } from './ada/types'; import type { ProviderEntity } from './entities/provider-entity'; import { InvalidNetworkError } from './errors/invalid-network-error'; import type { NetworkData, NetworkKey } from './networks'; import { getNetworkKeyById, isValidNetwork } from './networks'; import { PolkadotProvider } from './substrate/dot'; import { KusamaProvider } from './substrate/ksm'; +import type { SubstrateProviderProps } from './substrate/types'; import type { ProviderBuilderProps } from './types'; export class Web3Provider { @@ -21,16 +24,17 @@ export class Web3Provider { this.network = network } - private getAvailableProviders() { - return { - dot: PolkadotProvider, - ksm: KusamaProvider, + // TODO: Improve props type to avoid generic for vanilla javascript users + build(props: ProviderBuilderProps): ProviderEntity { + switch (this.network) { + case 'dot': + return new PolkadotProvider(props as SubstrateProviderProps) + case 'ksm': + return new KusamaProvider(props as SubstrateProviderProps) + case 'ada': + return new CardanoProvider(props as CardanoProviderProps) + default: + throw new InvalidNetworkError() } } - - build(props: ProviderBuilderProps): ProviderEntity { - const Provider = this.getAvailableProviders()[this.network] - - return new Provider(props) - } }