Skip to content

Commit

Permalink
[Issue-3816] feat: support cardano
Browse files Browse the repository at this point in the history
  • Loading branch information
bluezdot committed Oct 29, 2024
1 parent db6312b commit a522dc9
Show file tree
Hide file tree
Showing 19 changed files with 559 additions and 53 deletions.
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,10 @@
"@polkadot/types-support": "^12.0.2",
"@polkadot/util": "^12.6.2",
"@polkadot/util-crypto": "^12.6.2",
"@subwallet/chain-list": "0.2.91",
"@subwallet/keyring": "^0.1.8-beta.0",
"@subwallet/chain-list": "file:../SubWallet-Chainlist/packages/chain-list/build/",
"@subwallet/keyring": "file:../SubWallet-Base/packages/keyring/build/",
"@subwallet/react-ui": "5.1.2-b79",
"@subwallet/ui-keyring": "0.1.8-beta.0",
"@subwallet/ui-keyring": "file:../SubWallet-Base/packages/ui-keyring/build/",
"@zondax/ledger-substrate": "0.44.2",
"babel-core": "^7.0.0-bridge.0",
"babel-jest": "^29.3.1",
Expand Down
1 change: 1 addition & 0 deletions packages/extension-base/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"@acala-network/api": "^5.0.2",
"@apollo/client": "^3.7.14",
"@azns/resolver-core": "^1.4.0",
"@blockfrost/blockfrost-js": "^5.6.0",
"@chainflip/sdk": "^1.6.0",
"@equilab/api": "~1.14.25",
"@ethereumjs/common": "^4.1.0",
Expand Down
3 changes: 2 additions & 1 deletion packages/extension-base/src/background/KoniTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,8 @@ export enum TransactionDirection {
export enum ChainType {
EVM = 'evm',
SUBSTRATE = 'substrate',
TON = 'ton'
TON = 'ton',
CARDANO = 'cardano' // todo: check to add
}

export enum ExtrinsicType {
Expand Down
6 changes: 4 additions & 2 deletions packages/extension-base/src/constants/signing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import { AccountChainType } from '@subwallet/extension-base/types';
export const SIGNING_COMPATIBLE_MAP: Record<ChainType, AccountChainType[]> = {
[ChainType.SUBSTRATE]: [AccountChainType.SUBSTRATE, AccountChainType.ETHEREUM],
[ChainType.EVM]: [AccountChainType.ETHEREUM],
[ChainType.TON]: [AccountChainType.TON]
[ChainType.TON]: [AccountChainType.TON],
[ChainType.CARDANO]: [AccountChainType.CARDANO]
};

export const LEDGER_SIGNING_COMPATIBLE_MAP: Record<ChainType, AccountChainType[]> = {
[ChainType.SUBSTRATE]: [AccountChainType.SUBSTRATE],
[ChainType.EVM]: [AccountChainType.ETHEREUM],
[ChainType.TON]: [AccountChainType.TON]
[ChainType.TON]: [AccountChainType.TON],
[ChainType.CARDANO]: [AccountChainType.CARDANO]
};
8 changes: 8 additions & 0 deletions packages/extension-base/src/koni/background/handlers/State.ts
Original file line number Diff line number Diff line change
Expand Up @@ -920,6 +920,14 @@ export default class KoniState {
return this.chainService.getTonApi(networkKey);
}

public getCardanoApiMap () {
return this.chainService.getCardanoApiMap();
}

public getCardanoApi (networkKey: string) {
return this.chainService.getCardanoApi(networkKey)
}

public getApiMap () {
return {
substrate: this.chainService.getSubstrateApiMap(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright 2019-2022 @subwallet/extension-base
// SPDX-License-Identifier: Apache-2.0

import {ASTAR_REFRESH_BALANCE_INTERVAL} from '@subwallet/extension-base/constants';
import {_CardanoApi} from '@subwallet/extension-base/services/chain-service/types';
import {BalanceItem, SusbcribeCardanoPalletBallance} from '@subwallet/extension-base/types';
import {APIItemState} from "@subwallet/extension-base/background/KoniTypes";
import {filterAssetsByChainAndType} from "@subwallet/extension-base/utils";
import {_AssetType} from "@subwallet/chain-list/types";

async function getCardanoBalance (addresses: string[], cardanoApi: _CardanoApi): Promise<bigint[]> {
return await Promise.all(addresses.map(async (address) => {
try {
return await cardanoApi.
} catch (e) {
return BigInt(0);
}
}))
}

export function subscribeCardanoBalance (params: SusbcribeCardanoPalletBallance) {
const { addresses, assetMap, callback, cardanoApi, chainInfo } = params;
const chain = chainInfo.slug;
const nativeTokenInfo = filterAssetsByChainAndType(assetMap, chain, [_AssetType.NATIVE, _AssetType.CIP26]);

function getBalance () {
getCardanoBalance(addresses, cardanoApi)
.then((balances) => {
return balances.map((balance, index): BalanceItem => {
return {
address: addresses[index],
tokenSlug: nativeTokenSlug,
state: APIItemState.READY,
free: balance.toString(),
locked: '0' // todo: check staking on cardano
}
})
})
}

getBalance();
const interval = setInterval(getBalance, ASTAR_REFRESH_BALANCE_INTERVAL);

return () => {
clearInterval(interval);
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@

import { _AssetType, _ChainAsset, _ChainInfo } from '@subwallet/chain-list/types';
import { APIItemState, ExtrinsicType } from '@subwallet/extension-base/background/KoniTypes';
import { _EvmApi, _SubstrateApi, _TonApi } from '@subwallet/extension-base/services/chain-service/types';
import { _getSubstrateGenesisHash, _isChainBitcoinCompatible, _isChainEvmCompatible, _isChainTonCompatible, _isPureEvmChain, _isPureTonChain } from '@subwallet/extension-base/services/chain-service/utils';
import { subscribeCardanoBalance } from '@subwallet/extension-base/services/balance-service/helpers/subscribe/cardano';
import { _CardanoApi, _EvmApi, _SubstrateApi, _TonApi } from '@subwallet/extension-base/services/chain-service/types';
import { _getSubstrateGenesisHash, _isChainBitcoinCompatible, _isChainCardanoCompatible, _isChainEvmCompatible, _isChainTonCompatible, _isPureCardanoChain, _isPureEvmChain, _isPureTonChain } from '@subwallet/extension-base/services/chain-service/utils';
import { AccountJson, BalanceItem } from '@subwallet/extension-base/types';
import { categoryAddresses, filterAssetsByChainAndType, pairToAccount } from '@subwallet/extension-base/utils';
import keyring from '@subwallet/ui-keyring';
Expand Down Expand Up @@ -40,14 +41,16 @@ export const getAccountJsonByAddress = (address: string): AccountJson | null =>

/** Filter addresses to subscribe by chain info */
const filterAddress = (addresses: string[], chainInfo: _ChainInfo): [string[], string[]] => {
const { bitcoin, evm, substrate, ton } = categoryAddresses(addresses);
const { bitcoin, cardano, evm, substrate, ton } = categoryAddresses(addresses);

if (_isChainEvmCompatible(chainInfo)) {
return [evm, [...bitcoin, ...substrate, ...ton]];
return [evm, [...bitcoin, ...substrate, ...ton, ...cardano]];
} else if (_isChainBitcoinCompatible(chainInfo)) {
return [bitcoin, [...evm, ...substrate, ...ton]];
return [bitcoin, [...evm, ...substrate, ...ton, ...cardano]];
} else if (_isChainTonCompatible(chainInfo)) {
return [ton, [...bitcoin, ...evm, ...substrate]];
return [ton, [...bitcoin, ...evm, ...substrate, ...cardano]];
} else if (_isChainCardanoCompatible(chainInfo)) {
return [cardano, [...bitcoin, ...evm, ...substrate, ...ton]];
} else {
const fetchList: string[] = [];
const unfetchList: string[] = [];
Expand Down Expand Up @@ -77,7 +80,7 @@ const filterAddress = (addresses: string[], chainInfo: _ChainInfo): [string[], s
}
});

return [fetchList, [...unfetchList, ...bitcoin, ...evm, ...ton]];
return [fetchList, [...unfetchList, ...bitcoin, ...evm, ...ton, ...cardano]];
}
};

Expand Down Expand Up @@ -123,6 +126,7 @@ export function subscribeBalance (
substrateApiMap: Record<string, _SubstrateApi>,
evmApiMap: Record<string, _EvmApi>,
tonApiMap: Record<string, _TonApi>,
cardanoApiMap: Record<string, _CardanoApi>,
callback: (rs: BalanceItem[]) => void,
extrinsicType?: ExtrinsicType
) {
Expand Down Expand Up @@ -169,6 +173,18 @@ export function subscribeBalance (
});
}

const cardanoApi = cardanoApiMap[chainSlug];

if (_isPureCardanoChain(chainInfo)) {
return subscribeCardanoBalance({
addresses: useAddresses,
assetMap: chainAssetMap,
callback,
chainInfo,
cardanoApi
});
}

const substrateApi = await substrateApiMap[chainSlug].isReady;

return subscribeSubstrateBalance(useAddresses, chainInfo, chainAssetMap, substrateApi, evmApi, callback, extrinsicType);
Expand Down
6 changes: 4 additions & 2 deletions packages/extension-base/src/services/balance-service/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,10 +212,11 @@ export class BalanceService implements StoppableServiceInterface {
const evmApiMap = this.state.chainService.getEvmApiMap();
const substrateApiMap = this.state.chainService.getSubstrateApiMap();
const tonApiMap = this.state.chainService.getTonApiMap();
const cardanoApiMap = this.state.chainService.getCardanoApiMap();

let unsub = noop;

unsub = subscribeBalance([address], [chain], [tSlug], assetMap, chainInfoMap, substrateApiMap, evmApiMap, tonApiMap, (result) => {
unsub = subscribeBalance([address], [chain], [tSlug], assetMap, chainInfoMap, substrateApiMap, evmApiMap, tonApiMap, cardanoApiMap, (result) => {
const rs = result[0];

if (rs.tokenSlug === tSlug) {
Expand Down Expand Up @@ -366,6 +367,7 @@ export class BalanceService implements StoppableServiceInterface {
const evmApiMap = this.state.chainService.getEvmApiMap();
const substrateApiMap = this.state.chainService.getSubstrateApiMap();
const tonApiMap = this.state.chainService.getTonApiMap();
const cardanoApiMap = this.state.chainService.getCardanoApiMap();

const activeChainSlugs = Object.keys(this.state.getActiveChainInfoMap());
const assetState = this.state.chainService.subscribeAssetSettings().value;
Expand All @@ -375,7 +377,7 @@ export class BalanceService implements StoppableServiceInterface {
})
.map((asset) => asset.slug);

const unsub = subscribeBalance(addresses, activeChainSlugs, assets, assetMap, chainInfoMap, substrateApiMap, evmApiMap, tonApiMap, (result) => {
const unsub = subscribeBalance(addresses, activeChainSlugs, assets, assetMap, chainInfoMap, substrateApiMap, evmApiMap, tonApiMap, cardanoApiMap, (result) => {
!cancel && this.setBalanceItem(result);
}, ExtrinsicType.TRANSFER_BALANCE);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// Copyright 2019-2022 @subwallet/extension-base authors & contributors
// SPDX-License-Identifier: Apache-2.0

import { BlockFrostAPI } from '@blockfrost/blockfrost-js';
import { _ApiOptions } from '@subwallet/extension-base/services/chain-service/handler/types';
import { _CardanoApi, _ChainConnectionStatus } from '@subwallet/extension-base/services/chain-service/types';
import { createPromiseHandler, PromiseHandler } from '@subwallet/extension-base/utils';
import { BehaviorSubject } from 'rxjs';

export const API_KEY = {
mainnet: 'mainnet6uE9JH3zGYquaxRKA7IMhEuzRUB58uGK',
testnet: 'preprodcnP5RADcrWMlf2cQe4ZKm4cjRvrBQFXM'
};

export class CardanoApi implements _CardanoApi {
chainSlug: string;
private api: CardanoApi;
apiUrl: string;
apiError?: string;
apiRetry = 0;
public readonly isApiConnectedSubject = new BehaviorSubject(false);
public readonly connectionStatusSubject = new BehaviorSubject(_ChainConnectionStatus.DISCONNECTED);
isApiReady = false;
isApiReadyOnce = false;
isReadyHandler: PromiseHandler<_CardanoApi>;

providerName: string;

constructor (chainSlug: string, apiUrl: string, { isTestnet, providerName }: _ApiOptions) {
this.chainSlug = chainSlug;
this.apiUrl = apiUrl;
this.providerName = providerName || 'unknown';
this.api = this.createProvider(isTestnet);
this.isReadyHandler = createPromiseHandler<_CardanoApi>();

this.connect();
}

get isApiConnected (): boolean {
return this.isApiConnectedSubject.getValue();
}

get connectionStatus (): _ChainConnectionStatus {
return this.connectionStatusSubject.getValue();
}

private updateConnectionStatus (status: _ChainConnectionStatus): void {
const isConnected = status === _ChainConnectionStatus.CONNECTED;

if (isConnected !== this.isApiConnectedSubject.value) {
this.isApiConnectedSubject.next(isConnected);
}

if (status !== this.connectionStatusSubject.value) {
this.connectionStatusSubject.next(status);
}
}

get isReady (): Promise<_CardanoApi> {
return this.isReadyHandler.promise;
}

async updateApiUrl (apiUrl: string) {
if (this.apiUrl === apiUrl) {
return;
}

await this.disconnect();

this.apiUrl = apiUrl;
this.api = this.createProvider();
}

async recoverConnect () {
await this.disconnect();
this.connect();

await this.isReadyHandler.promise;
}

private createProvider (isTestnet = true) {
const projectId = isTestnet ? API_KEY.testnet : API_KEY.mainnet;

return new BlockFrostAPI({
projectId
});
}

private getJsonRpc (url: string) {
return `${url}/jsonRPC`;
}

connect (): void {
this.updateConnectionStatus(_ChainConnectionStatus.CONNECTING);
// There isn't a persistent network connection underlying TonClient. Cant check connection status.
// this.isApiReadyOnce = true;
this.onConnect();
}

async disconnect () {
this.onDisconnect();
this.updateConnectionStatus(_ChainConnectionStatus.DISCONNECTED);

return Promise.resolve();
}

destroy () {
// Todo: implement this in the future
return this.disconnect();
}

onConnect (): void {
if (!this.isApiConnected) {
console.log(`Connected to ${this.chainSlug} at ${this.apiUrl}`);
this.isApiReady = true;

if (this.isApiReadyOnce) {
this.isReadyHandler.resolve(this);
}
}

this.updateConnectionStatus(_ChainConnectionStatus.CONNECTED);
}

onDisconnect (): void {
this.updateConnectionStatus(_ChainConnectionStatus.DISCONNECTED);

if (this.isApiConnected) {
console.warn(`Disconnected from ${this.chainSlug} of ${this.apiUrl}`);
this.isApiReady = false;
this.isReadyHandler = createPromiseHandler<_CardanoApi>();
}
}
}
Loading

0 comments on commit a522dc9

Please sign in to comment.