Skip to content

Commit

Permalink
Add method signPsbt for bitcoin provider
Browse files Browse the repository at this point in the history
  • Loading branch information
Thiendekaco committed Jun 13, 2024
1 parent 50fe756 commit 23a85ad
Show file tree
Hide file tree
Showing 7 changed files with 196 additions and 23 deletions.
31 changes: 29 additions & 2 deletions packages/extension-base/src/background/KoniTypes.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright 2019-2022 @polkadot/extension-koni authors & contributors
// SPDX-License-Identifier: Apache-2.0

import type { Psbt, PsbtTxOutput } from 'bitcoinjs-lib';

import { _AssetRef, _AssetType, _ChainAsset, _ChainInfo, _FundStatus, _MultiChainAsset } from '@subwallet/chain-list/types';
import { TransactionError } from '@subwallet/extension-base/background/errors/TransactionError';
import { AuthUrls, Resolver } from '@subwallet/extension-base/background/handlers/State';
Expand All @@ -16,6 +18,7 @@ import { KeypairType, KeyringPair$Json, KeyringPair$Meta } from '@subwallet/keyr
import { KeyringOptions } from '@subwallet/ui-keyring/options/types';
import { KeyringAddress, KeyringPairs$Json } from '@subwallet/ui-keyring/types';
import { SessionTypes } from '@walletconnect/types/dist/types/sign-client/session';
import { PsbtTxInput } from 'bitcoinjs-lib/src/psbt';
import { DexieExportJsonStructure } from 'dexie-export-import';
import Web3 from 'web3';
import { RequestArguments, TransactionConfig } from 'web3-core';
Expand Down Expand Up @@ -1334,6 +1337,25 @@ export interface BitcoinSignRequest {
canSign: boolean;
}

export interface BitcoinSignPsbtConfirmRequest {
accounts: AccountJson[];
payload: BitcoinSignPsbtPayload
}

export interface BitcoinSignPsbtPayload {
txInput: PsbtTxInput[];
signingIndexes: Record<string, number[]>
txOutput: PsbtTxOutput[];
broadcast: boolean,
psbt: Psbt
}

export interface BitcoinSignPsbtRawRequest {
psbt: string;
signInputs: Record<string, number[]>;
broadcast: boolean;
}

export interface EvmSignatureRequest extends EvmSignRequest {
id: string;
type: string;
Expand All @@ -1355,7 +1377,7 @@ export type BitcoinSendTransactionRequest = BitcoinSignRequest

export type EvmWatchTransactionRequest = EvmSendTransactionRequest;
export type BitcoinWatchTransactionRequest = BitcoinSendTransactionRequest;
export type BitcoinSignPsbtRequest = BitcoinSendTransactionRequest;
export type BitcoinSignPsbtRequest = Omit<BitcoinSendTransactionRequest, 'account'> & BitcoinSignPsbtConfirmRequest;

export interface ConfirmationsQueueItemOptions {
requiredPassword?: boolean;
Expand All @@ -1368,6 +1390,11 @@ export interface SignMessageBitcoinResult {
address: string;
}

export interface SignPsbtBitcoinResult {
psbt: string;
txid: string
}

export interface ConfirmationsQueueItem<T> extends ConfirmationsQueueItemOptions, ConfirmationRequestBase {
payload: T;
payloadJson: string;
Expand Down Expand Up @@ -1435,7 +1462,7 @@ export interface ConfirmationDefinitionsBitcoin {
bitcoinSignatureRequest: [ConfirmationsQueueItem<BitcoinSignatureRequest>, ConfirmationResult<SignMessageBitcoinResult>],
bitcoinSendTransactionRequest: [ConfirmationsQueueItem<BitcoinSendTransactionRequest>, ConfirmationResult<string>],
bitcoinWatchTransactionRequest: [ConfirmationsQueueItem<BitcoinWatchTransactionRequest>, ConfirmationResult<string>],
bitcoinSignPsbtRequest: [ConfirmationsQueueItem<BitcoinSignPsbtRequest>, ConfirmationResult<string>],
bitcoinSignPsbtRequest: [ConfirmationsQueueItem<BitcoinSignPsbtRequest>, ConfirmationResult<SignPsbtBitcoinResult>],
}

export type ConfirmationType = keyof ConfirmationDefinitions;
Expand Down
79 changes: 76 additions & 3 deletions packages/extension-base/src/koni/background/handlers/State.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { BitcoinProviderError } from '@subwallet/extension-base/background/error
import { EvmProviderError } from '@subwallet/extension-base/background/errors/EvmProviderError';
import { withErrorLog } from '@subwallet/extension-base/background/handlers/helpers';
import { isSubscriptionRunning, unsubscribe } from '@subwallet/extension-base/background/handlers/subscriptions';
import { AccountRefMap, AddTokenRequestExternal, AmountData, APIItemState, ApiMap, AuthRequestV2, BasicTxErrorType, BitcoinProviderErrorType, BitcoinSignatureRequest, ChainStakingMetadata, ChainType, ConfirmationsQueue, CrowdloanItem, CrowdloanJson, CurrentAccountInfo, CurrentAccountProxyInfo, EvmProviderErrorType, EvmSendTransactionParams, EvmSendTransactionRequest, EvmSignatureRequest, ExternalRequestPromise, ExternalRequestPromiseStatus, ExtrinsicType, MantaAuthorizationContext, MantaPayConfig, MantaPaySyncState, NftCollection, NftItem, NftJson, NominatorMetadata, RequestAccountExportPrivateKey, RequestCheckPublicAndSecretKey, RequestConfirmationComplete, RequestConfirmationCompleteBitcoin, RequestCrowdloanContributions, RequestSettingsType, ResponseAccountExportPrivateKey, ResponseCheckPublicAndSecretKey, ServiceInfo, SignMessageBitcoinResult, SingleModeJson, StakingItem, StakingJson, StakingRewardItem, StakingRewardJson, StakingType, UiSettings } from '@subwallet/extension-base/background/KoniTypes';
import { AccountRefMap, AddTokenRequestExternal, AmountData, APIItemState, ApiMap, AuthRequestV2, BasicTxErrorType, BitcoinProviderErrorType, BitcoinSignatureRequest, BitcoinSignPsbtPayload, BitcoinSignPsbtRawRequest, BitcoinSignPsbtRequest, ChainStakingMetadata, ChainType, ConfirmationsQueue, CrowdloanItem, CrowdloanJson, CurrentAccountInfo, CurrentAccountProxyInfo, EvmProviderErrorType, EvmSendTransactionParams, EvmSendTransactionRequest, EvmSignatureRequest, ExternalRequestPromise, ExternalRequestPromiseStatus, ExtrinsicType, MantaAuthorizationContext, MantaPayConfig, MantaPaySyncState, NftCollection, NftItem, NftJson, NominatorMetadata, RequestAccountExportPrivateKey, RequestCheckPublicAndSecretKey, RequestConfirmationComplete, RequestConfirmationCompleteBitcoin, RequestCrowdloanContributions, RequestSettingsType, ResponseAccountExportPrivateKey, ResponseCheckPublicAndSecretKey, ServiceInfo, SignMessageBitcoinResult, SignPsbtBitcoinResult, SingleModeJson, StakingItem, StakingJson, StakingRewardItem, StakingRewardJson, StakingType, UiSettings } from '@subwallet/extension-base/background/KoniTypes';
import { AccountJson, RequestAuthorizeTab, RequestRpcSend, RequestRpcSubscribe, RequestRpcUnsubscribe, RequestSign, ResponseRpcListProviders, ResponseSigning } from '@subwallet/extension-base/background/types';
import { ALL_ACCOUNT_KEY, ALL_GENESIS_HASH, MANTA_PAY_BALANCE_INTERVAL } from '@subwallet/extension-base/constants';
import { BalanceService } from '@subwallet/extension-base/services/balance-service';
Expand Down Expand Up @@ -48,6 +48,7 @@ import { decodePair } from '@subwallet/keyring/pair/decode';
import { KeypairType } from '@subwallet/keyring/types';
import { keyring } from '@subwallet/ui-keyring';
import BigN from 'bignumber.js';
import * as bitcoin from 'bitcoinjs-lib';
import BN from 'bn.js';
import SimpleKeyring from 'eth-simple-keyring';
import { t } from 'i18next';
Expand Down Expand Up @@ -1168,8 +1169,8 @@ export default class KoniState {
} as ApiMap;
}

public async bitcoinSign (id: string, url: string, method: string, params: any, allowedAccounts: string[]): Promise<string | undefined | SignMessageBitcoinResult> {
const { address, message } = params as Record<string, string>;
public async bitcoinSign (id: string, url: string, method: string, params: Record<string, string>, allowedAccounts: string[]): Promise<string | undefined | SignMessageBitcoinResult> {
const { address, message } = params;

if (address === '' || !message) {
throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, t('Not found address or payload to sign'));
Expand Down Expand Up @@ -1221,6 +1222,78 @@ export default class KoniState {
});
}

public async bitcoinSignPspt (id: string, url: string, method: string, params: BitcoinSignPsbtRawRequest, allowedAccounts: string[]): Promise<string | undefined | SignMessageBitcoinResult | SignPsbtBitcoinResult> {
const { broadcast, psbt, signInputs } = params;

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

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

let canSign = true;

const accountListJson = Object.keys(signInputs).reduce((accountList, address) => {
if (!isBitcoinAddress(address)) {
return accountList;
}

// Check sign abiblity
if (!allowedAccounts.find((acc) => (acc.toLowerCase() === address.toLowerCase()))) {
return accountList;
}

const pair = keyring.getPair(address);

if (!pair) {
return accountList;
}

if (pair.meta && pair.meta.isExternal) {
canSign = false;
}

return [...accountList, { address: pair.address, ...pair.meta }];
}, [] as AccountJson[]);

const psbtGenerate = bitcoin.Psbt.fromHex(psbt);
const psbtTxInputs = psbtGenerate.txInputs;
const psbtTxOutputs = psbtGenerate.txOutputs;

const payload: BitcoinSignPsbtPayload = {
psbt: psbtGenerate,
broadcast,
signingIndexes: signInputs,
txInput: psbtTxInputs,
txOutput: psbtTxOutputs
};
const hashPayload = '';

const signPayload: BitcoinSignPsbtRequest = {
accounts: accountListJson,
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 refreshSubstrateApi (key: string) {
this.chainService.refreshSubstrateApi(key);

Expand Down
21 changes: 18 additions & 3 deletions packages/extension-base/src/koni/background/handlers/Tabs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { EvmProviderError } from '@subwallet/extension-base/background/errors/Ev
import { withErrorLog } from '@subwallet/extension-base/background/handlers/helpers';
import { AuthUrlInfo } from '@subwallet/extension-base/background/handlers/State';
import { createSubscription, unsubscribe } from '@subwallet/extension-base/background/handlers/subscriptions';
import { AddNetworkRequestExternal, AddTokenRequestExternal, AuthAddress, BitcoinProviderErrorType, EvmAppState, EvmEventType, EvmProviderErrorType, EvmSendTransactionParams, PassPhishing, RequestAddPspToken, RequestEvmProviderSend, RequestSettingsType, ValidateNetworkResponse } from '@subwallet/extension-base/background/KoniTypes';
import { AddNetworkRequestExternal, AddTokenRequestExternal, AuthAddress, BitcoinProviderErrorType, BitcoinSignPsbtRawRequest, EvmAppState, EvmEventType, EvmProviderErrorType, EvmSendTransactionParams, PassPhishing, RequestAddPspToken, RequestEvmProviderSend, RequestSettingsType, ValidateNetworkResponse } from '@subwallet/extension-base/background/KoniTypes';
import RequestBytesSign from '@subwallet/extension-base/background/RequestBytesSign';
import RequestExtrinsicSign from '@subwallet/extension-base/background/RequestExtrinsicSign';
import { AccountAuthType, MessageTypes, RequestAccountList, RequestAccountSubscribe, RequestAccountUnsubscribe, RequestAuthorizeTab, RequestRpcSend, RequestRpcSubscribe, RequestRpcUnsubscribe, RequestTypes, ResponseRpcListProviders, ResponseSigning, ResponseTypes, SubscriptionMessageTypes } from '@subwallet/extension-base/background/types';
Expand Down Expand Up @@ -1165,12 +1165,24 @@ export default class KoniTabs {
private async bitcoinSign (id: string, url: string, { method, params }: RequestArguments) {
const allowedAccounts = (await this.getBitcoinCurrentAccount(url));

const signResult = await this.#koniState.bitcoinSign(id, url, method, params, allowedAccounts);
const signResult = await this.#koniState.bitcoinSign(id, url, method, params as Record<string, string>, allowedAccounts);

if (signResult) {
return signResult;
} else {
throw new EvmProviderError(EvmProviderErrorType.INVALID_PARAMS, 'Failed to sign message');
throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, 'Failed to sign message');
}
}

private async bitcoinSignPspt (id: string, url: string, { method, params }: RequestArguments) {
const allowedAccounts = (await this.getBitcoinCurrentAccount(url));

const signResult = await this.#koniState.bitcoinSignPspt(id, url, method, params as BitcoinSignPsbtRawRequest, allowedAccounts);

if (signResult) {
return signResult;
} else {
throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, 'Failed to sign message');
}
}

Expand All @@ -1185,6 +1197,9 @@ export default class KoniTabs {
case 'signMessage':
return await this.bitcoinSign(id, url, request);

case 'signPsbt':
return await this.bitcoinSignPspt(id, url, request);

default:
return this.performWeb3Method(id, url, request);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright 2019-2022 @subwallet/extension-base authors & contributors
// SPDX-License-Identifier: Apache-2.0

import { ConfirmationDefinitionsBitcoin, ConfirmationsQueueBitcoin, ConfirmationsQueueItemOptions, ConfirmationTypeBitcoin, RequestConfirmationCompleteBitcoin, SignMessageBitcoinResult } from '@subwallet/extension-base/background/KoniTypes';
import { ConfirmationDefinitionsBitcoin, ConfirmationsQueueBitcoin, ConfirmationsQueueItemOptions, ConfirmationTypeBitcoin, RequestConfirmationCompleteBitcoin, SignMessageBitcoinResult, SignPsbtBitcoinResult } from '@subwallet/extension-base/background/KoniTypes';
import { ConfirmationRequestBase, Resolver } from '@subwallet/extension-base/background/types';
import RequestService from '@subwallet/extension-base/services/request-service';
import { isInternalRequest } from '@subwallet/extension-base/utils/request';
Expand Down Expand Up @@ -191,12 +191,45 @@ export default class BitcoinRequestHandler {
return signedTransaction.extractTransaction().toHex();
}

private bitcoinSignPsbtconfirmation (request: ConfirmationDefinitionsBitcoin['bitcoinSignPsbtRequest'][0]): SignPsbtBitcoinResult {
// Extract necessary information from the BitcoinSendTransactionRequest
const { accounts, payload } = request.payload;
const { psbt, signingIndexes } = payload;

const psbtList = accounts.map(({ address }) => {
const pair = keyring.getPair(address);

// Unlock the pair if it is locked
if (pair.isLocked) {
keyring.unlockPair(pair.address);
}

// Finalize all inputs in the Psbt

// Sign the Psbt using the pair's bitcoin object
const signedTransaction = pair.bitcoin.signTransaction(psbt, signingIndexes[address]);

return signedTransaction.finalizeAllInputs();
});

const psbtCombine = new Psbt().combine(...psbtList);

const transactionObj = psbt.extractTransaction();

return {
psbt: psbtCombine.toHex(),
txid: transactionObj.toHex()
};
}

private async decorateResultBitcoin<T extends ConfirmationTypeBitcoin> (t: T, request: ConfirmationDefinitionsBitcoin[T][0], result: ConfirmationDefinitionsBitcoin[T][1]) {
if (!result.payload) {
if (t === 'bitcoinSignatureRequest') {
result.payload = this.signMessageBitcoin(request as ConfirmationDefinitionsBitcoin['bitcoinSignatureRequest'][0]);
} else if (t === 'bitcoinSendTransactionRequest') {
result.payload = this.signTransactionBitcoin(request as ConfirmationDefinitionsBitcoin['bitcoinSendTransactionRequest'][0]);
} else if (t === 'bitcoinSignPsbtRequest') {
result.payload = this.bitcoinSignPsbtconfirmation(request as ConfirmationDefinitionsBitcoin['bitcoinSignPsbtRequest'][0]);
}

if (t === 'bitcoinSignatureRequest' || t === 'bitcoinSendTransactionRequest') {
Expand Down
11 changes: 10 additions & 1 deletion packages/extension-koni-ui/src/Popup/Confirmations/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ const Component = function ({ className }: Props) {
canSign = request.payload.canSign;
isMessage = confirmation.type === 'evmSignatureRequest';
} else if (['bitcoinSignatureRequest', 'bitcoinSendTransactionRequest', 'bitcoinWatchTransactionRequest'].includes(confirmation.type)) {
const request = confirmation.item as ConfirmationDefinitionsBitcoin['bitcoinSignatureRequest' | 'bitcoinSendTransactionRequest' | 'bitcoinWatchTransactionRequest' | 'bitcoinSignPsbtRequest'][0];
const request = confirmation.item as ConfirmationDefinitionsBitcoin['bitcoinSignatureRequest' | 'bitcoinSendTransactionRequest' | 'bitcoinWatchTransactionRequest'][0];

account = request.payload.account;
canSign = request.payload.canSign;
Expand Down Expand Up @@ -149,6 +149,15 @@ const Component = function ({ className }: Props) {
type={confirmation.type}
/>
);

case 'bitcoinSignPsbtRequest':
return (
<BitcoinSignatureConfirmation
request={confirmation.item as ConfirmationDefinitionsBitcoin['bitcoinSignPsbtRequest'][0]}
type={confirmation.type}
/>
);

case 'authorizeRequest':
return (
<AuthorizeConfirmation request={confirmation.item as AuthorizeRequest} />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors
// SPDX-License-Identifier: Apache-2.0

import { ConfirmationDefinitionsBitcoin, ConfirmationResult, EvmSendTransactionRequest, ExtrinsicType } from '@subwallet/extension-base/background/KoniTypes';
import { BitcoinSignatureRequest, BitcoinSignPsbtRequest, ConfirmationDefinitionsBitcoin, ConfirmationResult, EvmSendTransactionRequest, ExtrinsicType } from '@subwallet/extension-base/background/KoniTypes';
import { CONFIRMATION_QR_MODAL } from '@subwallet/extension-koni-ui/constants';
import { useGetChainInfoByChainId, useLedger, useNotification, useUnlockChecker } from '@subwallet/extension-koni-ui/hooks';
import { completeConfirmationBitcoin } from '@subwallet/extension-koni-ui/messaging';
Expand Down Expand Up @@ -50,7 +50,10 @@ const handleSignature = async (type: BitcoinSignatureSupportType, id: string, si

const Component: React.FC<Props> = (props: Props) => {
const { className, extrinsicType, id, payload, type } = props;
const { payload: { account, canSign, hashPayload } } = payload;
const { payload: { canSign, hashPayload } } = payload;
const account = (payload.payload as BitcoinSignatureRequest).account;
const accounts = (payload.payload as BitcoinSignPsbtRequest).accounts;
// const isModeAllAccount = accounts && accounts.length > 0 && !account;
const chainId = (payload.payload as EvmSendTransactionRequest)?.chainId || 1;

const { t } = useTranslation();
Expand All @@ -61,7 +64,7 @@ const Component: React.FC<Props> = (props: Props) => {
const chain = useGetChainInfoByChainId(chainId);
const checkUnlock = useUnlockChecker();

const signMode = useMemo(() => getSignMode(account), [account]);
const signMode = useMemo(() => getSignMode(account || accounts[0]?.address), [account, accounts]);
const isLedger = useMemo(() => signMode === AccountSignMode.LEDGER, [signMode]);
const isMessage = isBitcoinMessage(payload);

Expand Down
Loading

0 comments on commit 23a85ad

Please sign in to comment.