Skip to content

[Issue - 4245] Extension - Support connect dApp for Bitcoin #4397

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

Open
wants to merge 5 commits into
base: koni/dev/issue-4263
Choose a base branch
from
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
32 changes: 22 additions & 10 deletions packages/extension-base/src/background/KoniTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { RequestSubmitSignPsbtTransfer, RequestSubmitTransfer, RequestSubmitTran
import { RequestClaimBridge } from '@subwallet/extension-base/types/bridge';
import { GetNotificationParams, RequestIsClaimedPolygonBridge, RequestSwitchStatusParams } from '@subwallet/extension-base/types/notification';
import { InjectedAccount, InjectedAccountWithMeta, MetadataDefBase } from '@subwallet/extension-inject/types';
import { KeyringPair$Meta } from '@subwallet/keyring/types';
import { BitcoinAddressType, KeyringPair$Meta } from '@subwallet/keyring/types';
import { KeyringOptions } from '@subwallet/ui-keyring/options/types';
import { KeyringAddress } from '@subwallet/ui-keyring/types';
import { SessionTypes } from '@walletconnect/types/dist/types/sign-client/session';
Expand Down Expand Up @@ -1139,7 +1139,7 @@ export interface EvmSignRequest {
}

export interface BitcoinSignRequest {
account: AccountJson;
address: string;
hashPayload: string;
canSign: boolean;
}
Expand Down Expand Up @@ -1182,8 +1182,9 @@ export interface BitcoinSignPsbtRawRequest {
allowedSighash?: SignatureHash[];
signAtIndex?: number | number[];
broadcast?: boolean;
autoFinalized?: boolean;
network: string;
account: string;
address: string;
}

export interface TonSignRequest {
Expand Down Expand Up @@ -1311,6 +1312,23 @@ export interface BitcoinAppState {
listenEvents?: string[]
}

export type BitcoinDAppAddress = {
address: string;
publicKey?: string;
tweakedPublicKey?: string;
derivationPath?: string;
isTestnet?: boolean;
type: BitcoinAddressType;
}

export type BitcoinRequestGetAddressesResult = BitcoinDAppAddress[];

export interface BitcoinSignMessageResult {
signature: string;
message: string;
address: string;
}

// TODO: add account info + dataToSign
export type TonSendTransactionRequest = TonTransactionConfig;
export type CardanoSendTransactionRequest = CardanoTransactionConfig;
Expand Down Expand Up @@ -1340,12 +1358,6 @@ export interface BitcoinTransactionConfig{
tokenSlug?: string;
}

export interface SignMessageBitcoinResult {
signature: string;
message: string;
address: string;
}

export interface SignPsbtBitcoinResult {
psbt: string;
txid?: string
Expand Down Expand Up @@ -1417,7 +1429,7 @@ export interface ConfirmationDefinitionsCardano {
}

export interface ConfirmationDefinitionsBitcoin {
bitcoinSignatureRequest: [ConfirmationsQueueItem<BitcoinSignatureRequest>, ConfirmationResult<SignMessageBitcoinResult>],
bitcoinSignatureRequest: [ConfirmationsQueueItem<BitcoinSignatureRequest>, ConfirmationResult<BitcoinSignMessageResult>],
bitcoinSendTransactionRequest: [ConfirmationsQueueItem<BitcoinSendTransactionRequest>, ConfirmationResult<string>],
bitcoinSendTransactionRequestAfterConfirmation: [ConfirmationsQueueItem<BitcoinSendTransactionRequest>, ConfirmationResult<string>],
bitcoinWatchTransactionRequest: [ConfirmationsQueueItem<BitcoinWatchTransactionRequest>, ConfirmationResult<string>],
Expand Down
2 changes: 1 addition & 1 deletion packages/extension-base/src/background/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ export interface TransportRequestMessage<TMessageType extends MessageTypes> {
request: RequestTypes[TMessageType];
}

export type AccountAuthType = 'substrate' | 'evm' | 'ton' | 'cardano';
export type AccountAuthType = 'substrate' | 'evm' | 'ton' | 'cardano' | 'bitcoin';

export interface RequestAuthorizeTab {
origin: string;
Expand Down
84 changes: 80 additions & 4 deletions packages/extension-base/src/koni/background/handlers/Extension.ts

Large diffs are not rendered by default.

201 changes: 199 additions & 2 deletions packages/extension-base/src/koni/background/handlers/State.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@

import * as CardanoWasm from '@emurgo/cardano-serialization-lib-nodejs';
import { _AssetRef, _AssetType, _ChainAsset, _ChainInfo, _MultiChainAsset } from '@subwallet/chain-list/types';
import { BitcoinProviderError } from '@subwallet/extension-base/background/errors/BitcoinProviderError';
import { CardanoProviderError } from '@subwallet/extension-base/background/errors/CardanoProviderError';
import { EvmProviderError } from '@subwallet/extension-base/background/errors/EvmProviderError';
import { TransactionError } from '@subwallet/extension-base/background/errors/TransactionError';
import { withErrorLog } from '@subwallet/extension-base/background/handlers/helpers';
import { isSubscriptionRunning, unsubscribe } from '@subwallet/extension-base/background/handlers/subscriptions';
import { AddressCardanoTransactionBalance, AddTokenRequestExternal, AmountData, APIItemState, ApiMap, AuthRequestV2, CardanoKeyType, CardanoProviderErrorType, CardanoSignatureRequest, CardanoTransactionDappConfig, ChainStakingMetadata, ChainType, ConfirmationsQueue, ConfirmationsQueueBitcoin, ConfirmationsQueueCardano, ConfirmationsQueueTon, ConfirmationType, CrowdloanItem, CrowdloanJson, CurrencyType, EvmProviderErrorType, EvmSendTransactionParams, EvmSendTransactionRequest, EvmSignatureRequest, ExternalRequestPromise, ExternalRequestPromiseStatus, ExtrinsicType, MantaAuthorizationContext, MantaPayConfig, MantaPaySyncState, NftCollection, NftItem, NftJson, NominatorMetadata, RequestAccountExportPrivateKey, RequestCardanoSignData, RequestCardanoSignTransaction, RequestConfirmationComplete, RequestConfirmationCompleteBitcoin, RequestConfirmationCompleteCardano, RequestConfirmationCompleteTon, RequestCrowdloanContributions, RequestSettingsType, ResponseAccountExportPrivateKey, ResponseCardanoSignData, ResponseCardanoSignTransaction, ServiceInfo, SingleModeJson, StakingItem, StakingJson, StakingRewardItem, StakingRewardJson, StakingType, UiSettings } from '@subwallet/extension-base/background/KoniTypes';
import { AddressCardanoTransactionBalance, AddTokenRequestExternal, AmountData, APIItemState, ApiMap, AuthRequestV2, BitcoinProviderErrorType, BitcoinSignatureRequest, BitcoinSignMessageResult, BitcoinSignPsbtPayload, BitcoinSignPsbtRawRequest, BitcoinSignPsbtRequest, CardanoKeyType, CardanoProviderErrorType, CardanoSignatureRequest, CardanoTransactionDappConfig, ChainStakingMetadata, ChainType, ConfirmationsQueue, ConfirmationsQueueBitcoin, ConfirmationsQueueCardano, ConfirmationsQueueTon, ConfirmationType, CrowdloanItem, CrowdloanJson, CurrencyType, EvmProviderErrorType, EvmSendTransactionParams, EvmSendTransactionRequest, EvmSignatureRequest, ExternalRequestPromise, ExternalRequestPromiseStatus, ExtrinsicType, MantaAuthorizationContext, MantaPayConfig, MantaPaySyncState, NftCollection, NftItem, NftJson, NominatorMetadata, PsbtTransactionArg, RequestAccountExportPrivateKey, RequestCardanoSignData, RequestCardanoSignTransaction, RequestConfirmationComplete, RequestConfirmationCompleteBitcoin, RequestConfirmationCompleteCardano, RequestConfirmationCompleteTon, RequestCrowdloanContributions, RequestSettingsType, ResponseAccountExportPrivateKey, ResponseCardanoSignData, ResponseCardanoSignTransaction, ServiceInfo, SignPsbtBitcoinResult, SingleModeJson, StakingItem, StakingJson, StakingRewardItem, StakingRewardJson, StakingType, UiSettings } from '@subwallet/extension-base/background/KoniTypes';
import { RequestAuthorizeTab, RequestRpcSend, RequestRpcSubscribe, RequestRpcUnsubscribe, RequestSign, ResponseRpcListProviders, ResponseSigning } from '@subwallet/extension-base/background/types';
import { BACKEND_API_URL, BACKEND_PRICE_HISTORY_URL, EnvConfig, MANTA_PAY_BALANCE_INTERVAL, REMIND_EXPORT_ACCOUNT } from '@subwallet/extension-base/constants';
import { convertErrorFormat, generateValidationProcess, PayloadValidated, ValidateStepFunction, validationAuthMiddleware, validationAuthWCMiddleware, validationCardanoSignDataMiddleware, validationConnectMiddleware, validationEvmDataTransactionMiddleware, validationEvmSignMessageMiddleware } from '@subwallet/extension-base/core/logic-validation';
Expand Down Expand Up @@ -50,15 +51,19 @@ import { addLazy, isManifestV3, isSameAddress, reformatAddress, stripUrl, target
import { convertCardanoHexToBech32 } from '@subwallet/extension-base/utils/cardano';
import { createPromiseHandler } from '@subwallet/extension-base/utils/promise';
import { MetadataDef, ProviderMeta } from '@subwallet/extension-inject/types';
import { BitcoinMainnetKeypairTypes, BitcoinTestnetKeypairTypes } from '@subwallet/keyring/types';
import { isBitcoinAddress } from '@subwallet/keyring/utils/address/validate';
import subwalletApiSdk from '@subwallet/subwallet-api-sdk';
import { keyring } from '@subwallet/ui-keyring';
import BigN from 'bignumber.js';
import * as bitcoin from 'bitcoinjs-lib';
import BN from 'bn.js';
import { t } from 'i18next';
import { interfaces } from 'manta-extension-sdk';
import { BehaviorSubject, Subject } from 'rxjs';

import { JsonRpcResponse, ProviderInterface, ProviderInterfaceCallback } from '@polkadot/rpc-provider/types';
import { assert, logger as createLogger, noop } from '@polkadot/util';
import { assert, isArray, isHex, logger as createLogger, noop } from '@polkadot/util';
import { Logger } from '@polkadot/util/types';
import { isEthereumAddress } from '@polkadot/util-crypto';

Expand Down Expand Up @@ -1281,6 +1286,7 @@ export default class KoniState {
});
}

// Cardano
public async cardanoGetBalance (id: string, url: string, address: string): Promise<CardanoWasm.Value> {
const authInfoMap = await this.getAuthList();
const authInfo = authInfoMap[stripUrl(url)];
Expand Down Expand Up @@ -1534,6 +1540,197 @@ export default class KoniState {
return await cardanoApi.sendCardanoTxReturnHash(txHex);
}

// Bitcoin
public async bitcoinSign (id: string, url: string, method: string, params: Record<string, string>, allowedAccounts: string[]): Promise<BitcoinSignMessageResult> {
const { address, message } = params;

if (address === '' || !message) {
throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, t('Not found address or payload to sign'));
}

if (!isBitcoinAddress(address)) {
throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, t('Invalid bitcoin address'));
}

console.log(allowedAccounts);

// Check sign abiblity
if (!allowedAccounts.find((acc) => (acc.toLowerCase() === address.toLowerCase()))) {
throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, t('You have rescinded allowance for this account in wallet'));
}

const pair = keyring.getPair(address);

if (!pair) {
throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, t('Unable to find account'));
}

const hashPayload = '';
let canSign = false;

if (!pair?.meta.isExtneral) {
canSign = true;
}

const signPayload: BitcoinSignatureRequest = {
address,
payload: message as unknown,
payloadJson: message,
hashPayload,
canSign,
id
};

return this.requestService.addConfirmationBitcoin(id, url, 'bitcoinSignatureRequest', signPayload, {
requiredPassword: false,
address
})
.then(({ isApproved, payload }) => {
if (isApproved) {
if (payload) {
return payload;
} else {
throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, t('Not found signature'));
}
} else {
throw new BitcoinProviderError(BitcoinProviderErrorType.USER_REJECTED_REQUEST);
}
});
}

public async bitcoinSignPspt (id: string, url: string, networkKey: string, method: string, params: BitcoinSignPsbtRawRequest, allowedAccounts: string[]): Promise<SignPsbtBitcoinResult> {
const { address, allowedSighash, broadcast, psbt, signAtIndex } = params;

if (!psbt || !address) {
throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, t('Not found payload to sign'));
}

if (!isHex(`0x${psbt}`)) {
throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, t('Psbt to be signed must be hex-encoded'));
}

if (!isBitcoinAddress(address)) {
throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, t('Not found address'));
}

// Check sign abiblity
if (!allowedAccounts.find((acc) => (acc.toLowerCase() === address.toLowerCase()))) {
throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, t('You have rescinded allowance for this account in wallet'));
}

const pair = keyring.getPair(address);

if (!pair) {
throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, t('Unable to find account'));
}

if (networkKey === 'bitcoin') {
if (!BitcoinMainnetKeypairTypes.includes(pair.type)) {
throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, t('Your address is not on the mainnet network'));
}
} else if (networkKey === 'bitcoinTestnet') {
if (!BitcoinTestnetKeypairTypes.includes(pair.type)) {
throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, t('Your address is not on the testnet network'));
}
}

const network_ = networkKey === 'bitcoinTestnet' ? bitcoin.networks.testnet : bitcoin.networks.bitcoin;

const psbtGenerate = bitcoin.Psbt.fromHex(psbt, {
network: network_
});

const isExistedInput = (inputs: PsbtTransactionArg[], address: string) => inputs.findIndex(({ address: address_ }) => isSameAddress(address, address_ || ''));

const tokenInfo = this.getNativeTokenInfo(networkKey);
let to = '';
let value = new BigN(0);
const psbtInputData = psbtGenerate.data.inputs.reduce((inputs, { nonWitnessUtxo, witnessUtxo }, inputIndex) => {
let inputData: PsbtTransactionArg | null = null;

if (witnessUtxo) {
inputData = {
address: bitcoin.address.fromOutputScript(witnessUtxo?.script, network_),
amount: witnessUtxo.value.toString()
};
} else if (nonWitnessUtxo) {
const txin = psbtGenerate.txInputs[inputIndex];
const txout = bitcoin.Transaction.fromBuffer(nonWitnessUtxo).outs[txin.index];

inputData = {
address: bitcoin.address.fromOutputScript(txout.script, network_),
amount: txout.value.toString()
};
}

inputData && inputs.push(inputData);

return inputs;
}, [] as PsbtTransactionArg[]);

const psbtOutputData = psbtGenerate.txOutputs.map((output) => {
let address = '';

try {
address = output.address || bitcoin.address.fromOutputScript(output.script, network_);
} catch (e) {
if (output.script.includes(bitcoin.opcodes.OP_RETURN)) {
address = 'OP_RETURN';
} else {
address = 'Unknown';
}
}

if (isExistedInput(psbtInputData, address) === -1) {
to = address;
value = value.plus(new BigN(output.value));
}

return {
address,
amount: output.value.toString()
} as PsbtTransactionArg;
});

const payload: BitcoinSignPsbtPayload = {
psbt: psbtGenerate,
broadcast: !!broadcast,
value: value.toString(),
to,
network: networkKey,
signAtIndex: isArray(signAtIndex) && signAtIndex.length === 0 ? undefined : signAtIndex,
address,
allowedSighash,
tokenSlug: tokenInfo.slug,
txInput: psbtInputData,
txOutput: psbtOutputData
};
const hashPayload = '';
const canSign = !pair.meta.isExternal;

const signPayload: BitcoinSignPsbtRequest = {
address,
payload,
hashPayload,
canSign
};

return this.requestService.addConfirmationBitcoin(id, url, 'bitcoinSignPsbtRequest', signPayload, {
requiredPassword: false
})
.then(({ isApproved, payload }) => {
if (isApproved) {
if (payload) {
return payload;
} else {
throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, t('Not found signature'));
}
} else {
throw new BitcoinProviderError(BitcoinProviderErrorType.USER_REJECTED_REQUEST);
}
});
}

public getConfirmationsQueueSubject (): BehaviorSubject<ConfirmationsQueue> {
return this.requestService.confirmationsQueueSubject;
}
Expand Down
Loading