PHP WebShell
Текущая директория: /opt/BitGoJS/modules/sdk-core/src/bitgo/pendingApproval
Просмотр файла: pendingApproval.ts
/**
* @prettier
*/
import * as _ from 'lodash';
import * as utxolib from '@bitgo/utxo-lib';
import { IBaseCoin } from '../baseCoin';
import { BitGoBase } from '../bitgoBase';
import {
ApproveOptions,
IPendingApproval,
OwnerType,
PendingApprovalData,
PendingApprovalInfo,
State,
Type,
} from '../pendingApproval';
import { RequestTracer, RequestType } from '../utils';
import { IWallet } from '../wallet';
import { BuildParams } from '../wallet/BuildParams';
import { IRequestTracer } from '../../api';
import BaseTssUtils from '../utils/tss/baseTSSUtils';
import EddsaUtils from '../utils/tss/eddsa';
import { EcdsaMPCv2Utils, EcdsaUtils } from '../utils/tss/ecdsa';
import { KeyShare as EcdsaKeyShare } from '../utils/tss/ecdsa/types';
import { KeyShare as EddsaKeyShare } from '../utils/tss/eddsa/types';
import { sendTxRequest } from '../tss/common';
import assert from 'assert';
type PreApproveResult = {
txHex: string;
halfSigned?: string;
};
type ApprovePendingApprovalRequestBody = {
state: 'approved';
otp: string | undefined;
halfSigned?: string | Omit<PreApproveResult, 'halfSigned'>;
};
export class PendingApproval implements IPendingApproval {
private readonly bitgo: BitGoBase;
private readonly baseCoin: IBaseCoin;
private tssUtils?: BaseTssUtils<EcdsaKeyShare | EddsaKeyShare>;
private wallet?: IWallet;
private _pendingApproval: PendingApprovalData;
constructor(bitgo: BitGoBase, baseCoin: IBaseCoin, pendingApprovalData: PendingApprovalData, wallet?: IWallet) {
this.bitgo = bitgo;
this.baseCoin = baseCoin;
this.wallet = wallet;
if (this.baseCoin.supportsTss()) {
if (this.baseCoin.getMPCAlgorithm() === 'ecdsa') {
if (this.wallet?.multisigTypeVersion() === 'MPCv2') {
this.tssUtils = new EcdsaMPCv2Utils(this.bitgo, this.baseCoin, wallet);
} else {
this.tssUtils = new EcdsaUtils(this.bitgo, this.baseCoin, wallet);
}
} else {
this.tssUtils = new EddsaUtils(this.bitgo, this.baseCoin, wallet);
}
}
this._pendingApproval = pendingApprovalData;
}
/**
* Get the id for this PendingApproval
*/
id(): string {
return this._pendingApproval.id;
}
toJSON(): PendingApprovalData {
return this._pendingApproval;
}
/**
* Get the owner type (wallet or enterprise)
* Pending approvals can be approved or modified by different scopes (depending on how they were created)
* If a pending approval is owned by a wallet, then it can be approved by administrators of the wallet
* If a pending approval is owned by an enterprise, then it can be approved by administrators of the enterprise
*/
ownerType(): OwnerType {
if (this._pendingApproval.wallet) {
return OwnerType.WALLET;
} else if (this._pendingApproval.enterprise) {
return OwnerType.ENTERPRISE;
} else {
throw new Error('unexpected pending approval owner: neither wallet nor enterprise was present');
}
}
/**
* Get the id of the wallet which is associated with this PendingApproval
*/
walletId(): string | undefined {
return this._pendingApproval.wallet;
}
/**
* Get the enterprise ID that is associated with this PendingApproval
*/
enterpriseId(): string | undefined {
return this._pendingApproval.enterprise;
}
/**
* Get the state of this PendingApproval
*/
state(): State {
return this._pendingApproval.state;
}
/**
* Get the id of the user that performed the action resulting in this PendingApproval
*/
creator(): string {
return this._pendingApproval.creator;
}
/**
* Get the type of the pending approval (what it approves)
*/
type(): Type {
if (!this._pendingApproval.info) {
throw new Error('pending approval info is not available');
}
return this._pendingApproval.info.type;
}
/**
* Get information about this PendingApproval
*/
info(): PendingApprovalInfo {
return this._pendingApproval.info;
}
/**
* Get the number of approvals that are required for this PendingApproval to be approved.
* Defaults to 1 if approvalsRequired doesn't exist on the object
*/
approvalsRequired(): number {
return this._pendingApproval.approvalsRequired || 1;
}
/**
* Generate a url for this PendingApproval for making requests to the server.
* @param extra
*/
url(extra = ''): string {
return this.baseCoin.url('/pendingapprovals/' + this.id() + extra);
}
/**
* Refetches this PendingApproval from the server and returns it.
*
* Note that this mutates the PendingApproval object in place.
* @param params
*/
async get(params: Record<string, never> = {}): Promise<PendingApproval> {
this._pendingApproval = await this.bitgo.get(this.url()).result();
return this;
}
/**
* Sets this PendingApproval to an approved state
*/
async approve(params: ApproveOptions = {}): Promise<any> {
params.previewPendingTxs = true;
params.pendingApprovalId = this.id();
const canRecreateTransaction = this.canRecreateTransaction(params);
const reqId = new RequestTracer();
this.bitgo.setRequestTracer(reqId);
await this.populateWallet();
try {
const transaction = await this.preApprove(params, reqId);
const approvalParams: ApprovePendingApprovalRequestBody = { state: 'approved', otp: params.otp };
if (transaction) {
// if the transaction already has a half signed property, we take that directly
approvalParams.halfSigned = transaction.halfSigned || transaction;
}
const response = await this.bitgo.put(this.url()).send(approvalParams).result();
// if the response comes with an error, means that the transaction triggered another condition
if (response.hasOwnProperty('error') && response.hasOwnProperty('pendingApproval')) {
return response;
}
this._pendingApproval = response;
await this.postApprove(params, reqId);
return this._pendingApproval;
} catch (e) {
if (
!canRecreateTransaction &&
(e.message.indexOf('could not find unspent output for input') !== -1 ||
e.message.indexOf('transaction conflicts with an existing transaction in the send queue') !== -1)
) {
throw new Error('unspents expired, wallet passphrase or xprv required to recreate transaction');
}
throw e;
}
}
/**
* Sets this PendingApproval to a rejected state
* @param params
*/
async reject(params: Record<string, never> = {}): Promise<any> {
return await this.bitgo.put(this.url()).send({ state: 'rejected' }).result();
}
/**
* Alias for PendingApproval.reject()
*
* @deprecated
* @param params
*/
async cancel(params: Record<string, never> = {}): Promise<any> {
return await this.reject(params);
}
/**
* Recreate and sign TSS transaction
* @param {ApproveOptions} params needed to get txs and use the walletPassphrase to tss sign
* @param {RequestTracer} reqId id tracer.
*/
async recreateAndSignTSSTransaction(params: ApproveOptions, reqId: IRequestTracer): Promise<{ txHex: string }> {
const { walletPassphrase } = params;
const txRequestId = this._pendingApproval.txRequestId;
if (!this.wallet) {
throw new Error('Wallet not found');
}
if (!walletPassphrase) {
throw new Error('walletPassphrase not found');
}
if (!txRequestId) {
throw new Error('txRequestId not found');
}
const decryptedPrv = await this.wallet.getPrv({ walletPassphrase });
const txRequest = await this.tssUtils!.recreateTxRequest(txRequestId, decryptedPrv, reqId);
if (txRequest.apiVersion === 'lite') {
if (!txRequest.unsignedTxs || txRequest.unsignedTxs.length === 0) {
throw new Error('Unexpected error, no transactions found in txRequest.');
}
return {
txHex: txRequest.unsignedTxs[0].serializedTxHex,
};
} else {
if (!txRequest.transactions || txRequest.transactions.length === 0) {
throw new Error('Unexpected error, no transactions found in txRequest.');
}
return {
txHex: txRequest.transactions[0].unsignedTx.serializedTxHex,
};
}
}
/**
* Recreate a transaction for a pending approval to respond to updated network conditions
* @param params
* @param reqId
*/
async recreateAndSignTransaction(params: any = {}, reqId?: IRequestTracer): Promise<any> {
// this method only makes sense with existing transaction requests
const transactionRequest = this.info().transactionRequest;
if (_.isUndefined(transactionRequest)) {
throw new Error('cannot recreate transaction without transaction request');
}
if (_.isUndefined(this.wallet)) {
throw new Error('cannot recreate transaction without wallet');
}
const originalPrebuild = transactionRequest.coinSpecific[this.baseCoin.type];
const recipients = transactionRequest.recipients;
let prebuildParams = _.extend({}, params, { recipients: recipients }, transactionRequest.buildParams);
if (!_.isUndefined(originalPrebuild.hopTransaction)) {
prebuildParams.hop = true;
}
const reqTracer = reqId || new RequestTracer();
if (transactionRequest.buildParams && transactionRequest.buildParams.type === 'consolidate') {
// consolidate tag is in the build params - this is a consolidation transaction, so
// it needs to be rebuilt using the special consolidation build route
this.bitgo.setRequestTracer(reqTracer);
prebuildParams.prebuildTx = await this.bitgo
.post(this.wallet.url(`/consolidateUnspents`))
.send(BuildParams.encode(prebuildParams))
.result();
delete prebuildParams.recipients;
}
prebuildParams = _.extend({}, prebuildParams, { reqId: reqId });
const signedTransaction = await this.wallet.prebuildAndSignTransaction(prebuildParams);
// compare PAYGo fees
const originalParsedTransaction = (await this.baseCoin.parseTransaction({
txParams: prebuildParams,
wallet: this.wallet,
txPrebuild: originalPrebuild,
})) as any;
const recreatedParsedTransaction = (await this.baseCoin.parseTransaction({
txParams: prebuildParams,
wallet: this.wallet,
txPrebuild: signedTransaction,
})) as any;
if (_.isUndefined(recreatedParsedTransaction.implicitExternalSpendAmount)) {
return signedTransaction;
}
if (
typeof recreatedParsedTransaction.implicitExternalSpendAmount !== 'bigint' &&
!_.isFinite(recreatedParsedTransaction.implicitExternalSpendAmount)
) {
throw new Error('implicit external spend amount could not be determined');
}
if (
!_.isUndefined(originalParsedTransaction.implicitExternalSpendAmount) &&
recreatedParsedTransaction.implicitExternalSpendAmount > originalParsedTransaction.implicitExternalSpendAmount
) {
throw new Error('recreated transaction is using a higher pay-as-you-go-fee');
}
return signedTransaction;
}
/*
* Cold wallets cannot recreate transactions if the only thing provided is the wallet passphrase
*
* The transaction can be recreated if either
* – there is an xprv
* – there is a walletPassphrase and the wallet is not cold (because if it's cold, the passphrase is of little use)
*
* Therefore, if neither of these is true, the transaction cannot be recreated, which is reflected in the if
* statement below.
*
* Lightning transactions cannot be recreated.
*/
private canRecreateTransaction(params: ApproveOptions): boolean {
const isColdWallet = !!_.get(this.wallet, '_wallet.isCold');
const isOFCWallet = this.baseCoin.getFamily() === 'ofc'; // Off-chain transactions don't need to be rebuilt
const isLightningWallet = this.baseCoin.getFamily() === 'lnbtc';
if (isLightningWallet) {
return false;
}
if (!params.xprv && !(params.walletPassphrase && !isColdWallet && !isOFCWallet)) {
return false;
}
// If there are no recipients, then the transaction cannot be recreated
const recipients = this.info()?.transactionRequest?.buildParams?.recipients || [];
const type = this.info()?.transactionRequest?.buildParams?.type;
// We only want to not recreate transactions with no recipients if it is a UTXO coin.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return !(
utxolib.isValidNetwork((this.baseCoin as any).network) &&
recipients.length === 0 &&
type !== 'consolidate'
);
}
/*
* Internal helper function to get the serialized transaction which is being approved.
* If this PA is of type 'transactionRequest' this function will try to rebuild and resign the transaction
* @param {ApproveOptions} params
* @param {boolean} canRecreateTransaction -
* @param {RequestTracer} reqId id tracer
*/
private async preApprove(params: ApproveOptions = {}, reqId: IRequestTracer): Promise<PreApproveResult | undefined> {
// TransactionRequestLite or Multisig tx's must sign before pending approval is approved
// Re-signed tx is provided to the pending approval api
if (this.type() === Type.TRANSACTION_REQUEST) {
/*
* If this is a request for approving a transaction, depending on whether this user has a private key to the wallet
* (some admins may not have the spend permission), the transaction could either be rebroadcast as is, or it could
* be reconstructed. It is preferable to reconstruct a tx in order to adhere to the latest network conditions
* such as newer unspents, different fees, or a higher sequence id
*/
if (params.tx) {
// the approval tx was reconstructed and explicitly specified - pass it through
return {
txHex: params.tx,
};
}
// this user may not have spending privileges or a passphrase may not have been passed in
if (!this.canRecreateTransaction(params)) {
// If this is a TransactionRequest, then the txRequest already has the unsigned transaction
if (this._pendingApproval.txRequestId) {
return undefined;
}
// If this is a MultiSig, then we need to fetch the half signed tx to propagate to the approval API
const transaction = _.get(
this.info(),
`transactionRequest.coinSpecific.${this.baseCoin.type}`
) as PreApproveResult;
if (!_.isObject(transaction)) {
throw new Error('there is neither an original transaction object nor can a new one be recreated');
}
return transaction;
}
if (this._pendingApproval.txRequestId) {
return await this.recreateAndSignTSSTransaction(params, reqId);
}
return await this.recreateAndSignTransaction(params, reqId);
}
}
/**
* Internal helper function to perform any post-approval actions.
* If type is 'transactionRequestFull', this will sign the txRequestFull if possible
* @param params
* @param reqId
* @private
*/
private async postApprove(params: ApproveOptions = {}, reqId: IRequestTracer): Promise<void> {
switch (this.type()) {
case Type.TRANSACTION_REQUEST_FULL:
if (this._pendingApproval.state === State.APPROVED) {
// After we approve a lightning transaction, we should proceed with submitting the payment
if (this.baseCoin.getFamily() === 'lnbtc') {
assert(this._pendingApproval.txRequestId, 'Missing txRequestId');
// this.populateWallet is called before this so we should be good here
assert(this.wallet?.id(), 'Missing wallet id');
await sendTxRequest(
this.bitgo,
this.wallet?.id() as string,
this._pendingApproval.txRequestId,
RequestType.tx,
reqId
);
} else if (this.canRecreateTransaction(params) && this.baseCoin.supportsTss()) {
await this.recreateAndSignTSSTransaction(params, reqId);
}
}
}
}
/**
* Helper function to ensure that self.wallet is set
*/
private async populateWallet(): Promise<undefined> {
if (this.wallet) {
return;
}
// TODO(WP-1341): consolidate/simplify this logic
switch (this.type()) {
case Type.TRANSACTION_REQUEST:
const transactionRequest = this.info().transactionRequest;
if (_.isUndefined(transactionRequest)) {
throw new Error('missing required object property transactionRequest');
}
const updatedWallet: IWallet = await this.baseCoin.wallets().get({ id: transactionRequest.sourceWallet });
if (_.isUndefined(updatedWallet)) {
throw new Error('unexpected - unable to get wallet using sourcewallet');
}
this.wallet = updatedWallet;
if (this.wallet.id() !== transactionRequest.sourceWallet) {
throw new Error('unexpected source wallet for pending approval');
}
break;
case Type.TRANSACTION_REQUEST_FULL:
const walletId = this.walletId();
if (!walletId) {
throw new Error('Unexpected error, pendingApproval.wallet is expected to be defined!');
}
this.wallet = await this.baseCoin.wallets().get({ id: this.walletId() });
if (!this.wallet) {
throw new Error('unexpected - unable to get wallet using pendingApproval.wallet');
}
break;
}
return;
}
}
Выполнить команду
Для локальной разработки. Не используйте в интернете!