From 23a85adb3c0831dda19fcf6edd9ffc4cc4b66165 Mon Sep 17 00:00:00 2001 From: Thiendekaco Date: Thu, 13 Jun 2024 12:01:21 +0700 Subject: [PATCH] Add method signPsbt for bitcoin provider --- .../src/background/KoniTypes.ts | 31 +++++++- .../src/koni/background/handlers/State.ts | 79 ++++++++++++++++++- .../src/koni/background/handlers/Tabs.ts | 21 ++++- .../handler/BitcoinRequestHandler.ts | 35 +++++++- .../src/Popup/Confirmations/index.tsx | 11 ++- .../Confirmations/parts/Sign/Bitcoin.tsx | 9 ++- .../variants/BitcoinSignatureConfirmation.tsx | 33 +++++--- 7 files changed, 196 insertions(+), 23 deletions(-) diff --git a/packages/extension-base/src/background/KoniTypes.ts b/packages/extension-base/src/background/KoniTypes.ts index 800d21afd6..f271438076 100644 --- a/packages/extension-base/src/background/KoniTypes.ts +++ b/packages/extension-base/src/background/KoniTypes.ts @@ -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'; @@ -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'; @@ -1334,6 +1337,25 @@ export interface BitcoinSignRequest { canSign: boolean; } +export interface BitcoinSignPsbtConfirmRequest { + accounts: AccountJson[]; + payload: BitcoinSignPsbtPayload +} + +export interface BitcoinSignPsbtPayload { + txInput: PsbtTxInput[]; + signingIndexes: Record + txOutput: PsbtTxOutput[]; + broadcast: boolean, + psbt: Psbt +} + +export interface BitcoinSignPsbtRawRequest { + psbt: string; + signInputs: Record; + broadcast: boolean; +} + export interface EvmSignatureRequest extends EvmSignRequest { id: string; type: string; @@ -1355,7 +1377,7 @@ export type BitcoinSendTransactionRequest = BitcoinSignRequest export type EvmWatchTransactionRequest = EvmSendTransactionRequest; export type BitcoinWatchTransactionRequest = BitcoinSendTransactionRequest; -export type BitcoinSignPsbtRequest = BitcoinSendTransactionRequest; +export type BitcoinSignPsbtRequest = Omit & BitcoinSignPsbtConfirmRequest; export interface ConfirmationsQueueItemOptions { requiredPassword?: boolean; @@ -1368,6 +1390,11 @@ export interface SignMessageBitcoinResult { address: string; } +export interface SignPsbtBitcoinResult { + psbt: string; + txid: string +} + export interface ConfirmationsQueueItem extends ConfirmationsQueueItemOptions, ConfirmationRequestBase { payload: T; payloadJson: string; @@ -1435,7 +1462,7 @@ export interface ConfirmationDefinitionsBitcoin { bitcoinSignatureRequest: [ConfirmationsQueueItem, ConfirmationResult], bitcoinSendTransactionRequest: [ConfirmationsQueueItem, ConfirmationResult], bitcoinWatchTransactionRequest: [ConfirmationsQueueItem, ConfirmationResult], - bitcoinSignPsbtRequest: [ConfirmationsQueueItem, ConfirmationResult], + bitcoinSignPsbtRequest: [ConfirmationsQueueItem, ConfirmationResult], } export type ConfirmationType = keyof ConfirmationDefinitions; diff --git a/packages/extension-base/src/koni/background/handlers/State.ts b/packages/extension-base/src/koni/background/handlers/State.ts index 66efa107d1..8354010df8 100644 --- a/packages/extension-base/src/koni/background/handlers/State.ts +++ b/packages/extension-base/src/koni/background/handlers/State.ts @@ -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'; @@ -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'; @@ -1168,8 +1169,8 @@ export default class KoniState { } as ApiMap; } - public async bitcoinSign (id: string, url: string, method: string, params: any, allowedAccounts: string[]): Promise { - const { address, message } = params as Record; + public async bitcoinSign (id: string, url: string, method: string, params: Record, allowedAccounts: string[]): Promise { + const { address, message } = params; if (address === '' || !message) { throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, t('Not found address or payload to sign')); @@ -1221,6 +1222,78 @@ export default class KoniState { }); } + public async bitcoinSignPspt (id: string, url: string, method: string, params: BitcoinSignPsbtRawRequest, allowedAccounts: string[]): Promise { + 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); diff --git a/packages/extension-base/src/koni/background/handlers/Tabs.ts b/packages/extension-base/src/koni/background/handlers/Tabs.ts index 547b0d45aa..0e08a970de 100644 --- a/packages/extension-base/src/koni/background/handlers/Tabs.ts +++ b/packages/extension-base/src/koni/background/handlers/Tabs.ts @@ -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'; @@ -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, 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'); } } @@ -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); } diff --git a/packages/extension-base/src/services/request-service/handler/BitcoinRequestHandler.ts b/packages/extension-base/src/services/request-service/handler/BitcoinRequestHandler.ts index 650d6de236..635f32e4ca 100644 --- a/packages/extension-base/src/services/request-service/handler/BitcoinRequestHandler.ts +++ b/packages/extension-base/src/services/request-service/handler/BitcoinRequestHandler.ts @@ -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'; @@ -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: 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') { diff --git a/packages/extension-koni-ui/src/Popup/Confirmations/index.tsx b/packages/extension-koni-ui/src/Popup/Confirmations/index.tsx index 1cc6418b6e..275130ade6 100644 --- a/packages/extension-koni-ui/src/Popup/Confirmations/index.tsx +++ b/packages/extension-koni-ui/src/Popup/Confirmations/index.tsx @@ -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; @@ -149,6 +149,15 @@ const Component = function ({ className }: Props) { type={confirmation.type} /> ); + + case 'bitcoinSignPsbtRequest': + return ( + + ); + case 'authorizeRequest': return ( diff --git a/packages/extension-koni-ui/src/Popup/Confirmations/parts/Sign/Bitcoin.tsx b/packages/extension-koni-ui/src/Popup/Confirmations/parts/Sign/Bitcoin.tsx index 976049561d..5a4b0d23da 100644 --- a/packages/extension-koni-ui/src/Popup/Confirmations/parts/Sign/Bitcoin.tsx +++ b/packages/extension-koni-ui/src/Popup/Confirmations/parts/Sign/Bitcoin.tsx @@ -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'; @@ -50,7 +50,10 @@ const handleSignature = async (type: BitcoinSignatureSupportType, id: string, si const Component: React.FC = (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(); @@ -61,7 +64,7 @@ const Component: React.FC = (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); diff --git a/packages/extension-koni-ui/src/Popup/Confirmations/variants/BitcoinSignatureConfirmation.tsx b/packages/extension-koni-ui/src/Popup/Confirmations/variants/BitcoinSignatureConfirmation.tsx index 4cb70f36ba..e32341b34c 100644 --- a/packages/extension-koni-ui/src/Popup/Confirmations/variants/BitcoinSignatureConfirmation.tsx +++ b/packages/extension-koni-ui/src/Popup/Confirmations/variants/BitcoinSignatureConfirmation.tsx @@ -1,7 +1,7 @@ // Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { BitcoinSignatureRequest, ConfirmationsQueueItem } from '@subwallet/extension-base/background/KoniTypes'; +import { BitcoinSignatureRequest, BitcoinSignPsbtRequest, ConfirmationsQueueItem } from '@subwallet/extension-base/background/KoniTypes'; import { AccountItemWithName, ConfirmationGeneralInfo, MetaInfo, ViewDetailIcon } from '@subwallet/extension-koni-ui/components'; import { useOpenDetailModal } from '@subwallet/extension-koni-ui/hooks'; import { BitcoinSignArea } from '@subwallet/extension-koni-ui/Popup/Confirmations/parts'; @@ -16,13 +16,14 @@ import { BaseDetailModal } from '../parts'; interface Props extends ThemeProps { type: BitcoinSignatureSupportType - request: ConfirmationsQueueItem + request: ConfirmationsQueueItem | ConfirmationsQueueItem } function Component ({ className, request, type }: Props) { const { id, payload } = request; const { t } = useTranslation(); - const { account } = payload; + const { account } = payload as BitcoinSignatureRequest; + const { accounts } = payload as BitcoinSignPsbtRequest; const onClickDetail = useOpenDetailModal(); @@ -36,13 +37,25 @@ function Component ({ className, request, type }: Props) {
{t('You are approving a request with the following account')}
- + {account + ? + : accounts.map((account) => ( + + )) + }