PHP WebShell
Текущая директория: /opt/BitGoJS/modules/sdk-core/src/bitgo/wallet
Просмотр файла: wallets.ts
/**
* @prettier
*/
import assert from 'assert';
import { BigNumber } from 'bignumber.js';
import { bip32 } from '@bitgo/utxo-lib';
import * as _ from 'lodash';
import { CoinFeature } from '@bitgo/statics';
import { sanitizeLegacyPath } from '../../api';
import * as common from '../../common';
import { IBaseCoin, KeychainsTriplet, SupplementGenerateWalletOptions } from '../baseCoin';
import { BitGoBase } from '../bitgoBase';
import { getSharedSecret } from '../ecdh';
import { AddKeychainOptions, Keychain, KeyIndices } from '../keychain';
import { decodeOrElse, promiseProps, RequestTracer } from '../utils';
import {
AcceptShareOptions,
AcceptShareOptionsRequest,
AddWalletOptions,
BulkAcceptShareOptions,
BulkAcceptShareResponse,
BulkUpdateWalletShareOptions,
BulkUpdateWalletShareOptionsRequest,
BulkUpdateWalletShareResponse,
GenerateBaseMpcWalletOptions,
GenerateLightningWalletOptions,
GenerateLightningWalletOptionsCodec,
GenerateMpcWalletOptions,
GenerateSMCMpcWalletOptions,
GenerateWalletOptions,
GetWalletByAddressOptions,
GetWalletOptions,
IWallets,
LightningWalletWithKeychains,
ListWalletOptions,
UpdateShareOptions,
WalletShares,
WalletWithKeychains,
} from './iWallets';
import { WalletShare } from './iWallet';
import { Wallet } from './wallet';
import { TssSettings } from '@bitgo/public-types';
/**
* Check if a wallet is a WalletWithKeychains
*/
export function isWalletWithKeychains(
wallet: WalletWithKeychains | LightningWalletWithKeychains
): wallet is WalletWithKeychains {
return wallet.responseType === 'WalletWithKeychains';
}
export class Wallets implements IWallets {
private readonly bitgo: BitGoBase;
private readonly baseCoin: IBaseCoin;
constructor(bitgo: BitGoBase, baseCoin: IBaseCoin) {
this.bitgo = bitgo;
this.baseCoin = baseCoin;
}
/**
* Get a wallet by ID (proxy for getWallet)
* @param params
*/
async get(params: GetWalletOptions = {}): Promise<Wallet> {
return this.getWallet(params);
}
/**
* List a user's wallets
* @param params
* @returns {*}
*/
async list(params: ListWalletOptions & { enterprise?: string } = {}): Promise<{ wallets: Wallet[] }> {
if (params.skip && params.prevId) {
throw new Error('cannot specify both skip and prevId');
}
const body = (await this.bitgo.get(this.baseCoin.url('/wallet')).query(params).result()) as any;
body.wallets = body.wallets.map((w) => new Wallet(this.bitgo, this.baseCoin, w));
return body;
}
/**
* add
* Add a new wallet (advanced mode).
* This allows you to manually submit the keys, type, m and n of the wallet
* Parameters include:
* "label": label of the wallet to be shown in UI
* "m": number of keys required to unlock wallet (2)
* "n": number of keys available on the wallet (3)
* "keys": array of keychain ids
*/
async add(params: AddWalletOptions): Promise<any> {
params = params || {};
common.validateParams(params, [], ['label', 'enterprise', 'type']);
if (typeof params.label !== 'string') {
throw new Error('missing required string parameter label');
}
// no need to pass keys for (single) custodial wallets
if (params.type !== 'custodial') {
if (Array.isArray(params.keys) === false || !_.isNumber(params.m) || !_.isNumber(params.n)) {
throw new Error('invalid argument');
}
// TODO: support more types of multisig
if (!this.baseCoin.isValidMofNSetup(params)) {
throw new Error('unsupported multi-sig type');
}
}
if (params.gasPrice && !_.isNumber(params.gasPrice)) {
throw new Error('invalid argument for gasPrice - number expected');
}
if (params.walletVersion) {
if (!_.isNumber(params.walletVersion)) {
throw new Error('invalid argument for walletVersion - number expected');
}
if (params.multisigType === 'tss' && this.baseCoin.getMPCAlgorithm() === 'ecdsa' && params.walletVersion === 3) {
const tssSettings: TssSettings = await this.bitgo
.get(this.bitgo.microservicesUrl('/api/v2/tss/settings'))
.result();
const multisigTypeVersion =
tssSettings.coinSettings[this.baseCoin.getFamily()]?.walletCreationSettings?.multiSigTypeVersion;
if (multisigTypeVersion === 'MPCv2') {
params.walletVersion = 5;
}
}
}
if (params.tags && Array.isArray(params.tags) === false) {
throw new Error('invalid argument for tags - array expected');
}
if (params.clientFlags && Array.isArray(params.clientFlags) === false) {
throw new Error('invalid argument for clientFlags - array expected');
}
if (params.isCold && !_.isBoolean(params.isCold)) {
throw new Error('invalid argument for isCold - boolean expected');
}
if (params.isCustodial && !_.isBoolean(params.isCustodial)) {
throw new Error('invalid argument for isCustodial - boolean expected');
}
if (params.address && (!_.isString(params.address) || !this.baseCoin.isValidAddress(params.address))) {
throw new Error('invalid argument for address - valid address string expected');
}
const newWallet = await this.bitgo.post(this.baseCoin.url('/wallet/add')).send(params).result();
return {
wallet: new Wallet(this.bitgo, this.baseCoin, newWallet),
};
}
private async generateLightningWallet(params: GenerateLightningWalletOptions): Promise<LightningWalletWithKeychains> {
const reqId = new RequestTracer();
this.bitgo.setRequestTracer(reqId);
const { label, passphrase, enterprise, passcodeEncryptionCode, subType } = params;
// TODO BTC-1899: only userAuth key is required for custodial lightning wallet. all 3 keys are required for self custodial lightning.
// to avoid changing the platform for custodial flow, let us all 3 keys both wallet types.
const keychainPromises = ([undefined, 'userAuth', 'nodeAuth'] as const).map((purpose) => {
return async (): Promise<Keychain> => {
const keychain = this.baseCoin.keychains().create();
const keychainParams: AddKeychainOptions = {
pub: keychain.pub,
encryptedPrv: this.bitgo.encrypt({ password: passphrase, input: keychain.prv }),
originalPasscodeEncryptionCode: purpose === undefined ? passcodeEncryptionCode : undefined,
coinSpecific: purpose === undefined ? undefined : { [this.baseCoin.getChain()]: { purpose } },
keyType: 'independent',
source: 'user',
};
return await this.baseCoin.keychains().add(keychainParams);
};
});
const { userKeychain, userAuthKeychain, nodeAuthKeychain } = await promiseProps({
userKeychain: keychainPromises[0](),
userAuthKeychain: keychainPromises[1](),
nodeAuthKeychain: keychainPromises[2](),
});
const walletParams: SupplementGenerateWalletOptions = {
label,
m: 1,
n: 1,
type: 'hot',
subType,
enterprise,
keys: [userKeychain.id],
coinSpecific: { [this.baseCoin.getChain()]: { keys: [userAuthKeychain.id, nodeAuthKeychain.id] } },
};
const newWallet = await this.bitgo.post(this.baseCoin.url('/wallet/add')).send(walletParams).result();
const wallet = new Wallet(this.bitgo, this.baseCoin, newWallet);
return {
wallet,
userKeychain,
userAuthKeychain,
nodeAuthKeychain,
responseType: 'LightningWalletWithKeychains',
};
}
/**
* Generate a new wallet
* 1. Creates the user keychain locally on the client, and encrypts it with the provided passphrase
* 2. If no pub was provided, creates the backup keychain locally on the client, and encrypts it with the provided passphrase
* 3. Uploads the encrypted user and backup keychains to BitGo
* 4. Creates the BitGo key on the service
* 5. Creates the wallet on BitGo with the 3 public keys above
* @param params
* @param params.label Label for the wallet
* @param params.passphrase Passphrase to be used to encrypt the user and backup keychains
* @param params.userKey User xpub
* @param params.backupXpub Backup xpub
* @param params.backupXpubProvider
* @param params.enterprise the enterpriseId
* @param params.disableTransactionNotifications
* @param params.passcodeEncryptionCode optional this is a recovery code that can be used to decrypt the original passphrase in a recovery case.
* The user must generate and keep the encrypted original passphrase safe while this code is stored on BitGo
* @param params.coldDerivationSeed optional seed for SMC wallets
* @param params.gasPrice
* @param params.disableKRSEmail
* @param params.walletVersion
* @param params.multisigType optional multisig type, 'onchain' or 'tss' or 'blsdkg'; if absent, we will defer to the coin's default type
* @param params.isDistributedCustody optional parameter for creating bitgo key. This is only necessary if you want to create
* a distributed custody wallet. If provided, you must have the enterprise license and pass in
* `params.enterprise` into `generateWallet` as well.
* @param params.type optional wallet type, 'hot' or 'cold' or 'custodial'; if absent, we will defer to 'hot'
* @param params.bitgoKeyId optional bitgo key id for SMC TSS wallets
* @param params.commonKeychain optional common keychain for SMC TSS wallets
*
* @returns {*}
*/
async generateWallet(
params: GenerateWalletOptions = {}
): Promise<WalletWithKeychains | LightningWalletWithKeychains> {
// Assign the default multiSig type value based on the coin
if (!params.multisigType) {
params.multisigType = this.baseCoin.getDefaultMultisigType();
}
if (this.baseCoin.getFamily() === 'lnbtc') {
const options = decodeOrElse(
GenerateLightningWalletOptionsCodec.name,
GenerateLightningWalletOptionsCodec,
params,
(errors) => {
throw new Error(`error(s) parsing generate lightning wallet request params: ${errors}`);
}
);
const walletData = await this.generateLightningWallet(options);
walletData.encryptedWalletPassphrase = this.bitgo.encrypt({
input: options.passphrase,
password: options.passcodeEncryptionCode,
});
return walletData;
}
common.validateParams(params, ['label'], ['passphrase', 'userKey', 'backupXpub']);
if (typeof params.label !== 'string') {
throw new Error('missing required string parameter label');
}
const { type = 'hot', label, passphrase, enterprise, isDistributedCustody } = params;
const isTss = params.multisigType === 'tss' && this.baseCoin.supportsTss();
const canEncrypt = !!passphrase && typeof passphrase === 'string';
const walletParams: SupplementGenerateWalletOptions = {
label: label,
m: 2,
n: 3,
keys: [],
type: !!params.userKey && params.multisigType !== 'onchain' ? 'cold' : type,
};
if (!_.isUndefined(params.passcodeEncryptionCode)) {
if (!_.isString(params.passcodeEncryptionCode)) {
throw new Error('passcodeEncryptionCode must be a string');
}
}
if (!_.isUndefined(enterprise)) {
if (!_.isString(enterprise)) {
throw new Error('invalid enterprise argument, expecting string');
}
walletParams.enterprise = enterprise;
}
// EVM TSS wallets must use wallet version 3, 5 and 6
if (
isTss &&
this.baseCoin.isEVM() &&
!(params.walletVersion === 3 || params.walletVersion === 5 || params.walletVersion === 6)
) {
throw new Error('EVM TSS wallets are only supported for wallet version 3, 5 and 6');
}
if (isTss) {
if (!this.baseCoin.supportsTss()) {
throw new Error(`coin ${this.baseCoin.getFamily()} does not support TSS at this time`);
}
if (
(params.walletVersion === 5 || params.walletVersion === 6) &&
!this.baseCoin.getConfig().features.includes(CoinFeature.MPCV2)
) {
throw new Error(`coin ${this.baseCoin.getFamily()} does not support TSS MPCv2 at this time`);
}
assert(enterprise, 'enterprise is required for TSS wallet');
if (type === 'cold') {
// validate
assert(params.bitgoKeyId, 'bitgoKeyId is required for SMC TSS wallet');
assert(params.commonKeychain, 'commonKeychain is required for SMC TSS wallet');
return this.generateSMCMpcWallet({
multisigType: 'tss',
label,
enterprise,
walletVersion: params.walletVersion,
bitgoKeyId: params.bitgoKeyId,
commonKeychain: params.commonKeychain,
coldDerivationSeed: params.coldDerivationSeed,
});
}
if (type === 'custodial') {
return this.generateCustodialMpcWallet({
multisigType: 'tss',
label,
enterprise,
walletVersion: params.walletVersion,
});
}
assert(passphrase, 'cannot generate TSS keys without passphrase');
const walletData = await this.generateMpcWallet({
multisigType: 'tss',
label,
passphrase,
originalPasscodeEncryptionCode: params.passcodeEncryptionCode,
enterprise,
walletVersion: params.walletVersion,
});
if (params.passcodeEncryptionCode) {
walletData.encryptedWalletPassphrase = this.bitgo.encrypt({
input: passphrase,
password: params.passcodeEncryptionCode,
});
}
return walletData;
}
// Handle distributed custody
if (isDistributedCustody) {
if (!enterprise) {
throw new Error('must provide enterprise when creating distributed custody wallet');
}
if (!type || type !== 'cold') {
throw new Error('distributed custody wallets must be type: cold');
}
}
const hasBackupXpub = !!params.backupXpub;
const hasBackupXpubProvider = !!params.backupXpubProvider;
if (hasBackupXpub && hasBackupXpubProvider) {
throw new Error('Cannot provide more than one backupXpub or backupXpubProvider flag');
}
if (params.gasPrice && params.eip1559) {
throw new Error('can not use both eip1559 and gasPrice values');
}
if (!_.isUndefined(params.disableTransactionNotifications)) {
if (!_.isBoolean(params.disableTransactionNotifications)) {
throw new Error('invalid disableTransactionNotifications argument, expecting boolean');
}
walletParams.disableTransactionNotifications = params.disableTransactionNotifications;
}
if (!_.isUndefined(params.gasPrice)) {
const gasPriceBN = new BigNumber(params.gasPrice);
if (gasPriceBN.isNaN()) {
throw new Error('invalid gas price argument, expecting number or number as string');
}
walletParams.gasPrice = gasPriceBN.toString();
}
if (!_.isUndefined(params.eip1559) && !_.isEmpty(params.eip1559)) {
const maxFeePerGasBN = new BigNumber(params.eip1559.maxFeePerGas);
if (maxFeePerGasBN.isNaN()) {
throw new Error('invalid max fee argument, expecting number or number as string');
}
const maxPriorityFeePerGasBN = new BigNumber(params.eip1559.maxPriorityFeePerGas);
if (maxPriorityFeePerGasBN.isNaN()) {
throw new Error('invalid priority fee argument, expecting number or number as string');
}
walletParams.eip1559 = {
maxFeePerGas: maxFeePerGasBN.toString(),
maxPriorityFeePerGas: maxPriorityFeePerGasBN.toString(),
};
}
if (!_.isUndefined(params.disableKRSEmail)) {
if (!_.isBoolean(params.disableKRSEmail)) {
throw new Error('invalid disableKRSEmail argument, expecting boolean');
}
walletParams.disableKRSEmail = params.disableKRSEmail;
}
if (!_.isUndefined(params.walletVersion)) {
if (!_.isNumber(params.walletVersion)) {
throw new Error('invalid walletVersion provided, expecting number');
}
walletParams.walletVersion = params.walletVersion;
}
// Ensure each krsSpecific param is either a string, boolean, or number
const { krsSpecific } = params;
if (!_.isUndefined(krsSpecific)) {
Object.keys(krsSpecific).forEach((key) => {
const val = krsSpecific[key];
if (!_.isBoolean(val) && !_.isString(val) && !_.isNumber(val)) {
throw new Error('krsSpecific object contains illegal values. values must be strings, booleans, or numbers');
}
});
}
let derivationPath: string | undefined = undefined;
const reqId = new RequestTracer();
if (params.type === 'custodial' && (params.multisigType ?? 'onchain') === 'onchain') {
// for custodial multisig, when the wallet is created on the platfor side, the keys are not needed
walletParams.n = undefined;
walletParams.m = undefined;
walletParams.keys = undefined;
walletParams.keySignatures = undefined;
const newWallet = await this.bitgo.post(this.baseCoin.url('/wallet/add')).send(walletParams).result(); // returns the ids
const userKeychain = this.baseCoin.keychains().get({ id: newWallet.keys[KeyIndices.USER], reqId });
const backupKeychain = this.baseCoin.keychains().get({ id: newWallet.keys[KeyIndices.BACKUP], reqId });
const bitgoKeychain = this.baseCoin.keychains().get({ id: newWallet.keys[KeyIndices.BITGO], reqId });
const [userKey, bitgoKey, backupKey] = await Promise.all([userKeychain, bitgoKeychain, backupKeychain]);
const result: WalletWithKeychains = {
wallet: new Wallet(this.bitgo, this.baseCoin, newWallet),
userKeychain: userKey,
backupKeychain: bitgoKey,
bitgoKeychain: backupKey,
responseType: 'WalletWithKeychains',
};
return result;
} else {
const userKeychainPromise = async (): Promise<Keychain> => {
let userKeychainParams;
let userKeychain;
// User provided user key
if (params.userKey) {
userKeychain = { pub: params.userKey };
userKeychainParams = userKeychain;
if (params.coldDerivationSeed) {
// the derivation only makes sense when a key already exists
const derivation = this.baseCoin.deriveKeyWithSeed({
key: params.userKey,
seed: params.coldDerivationSeed,
});
derivationPath = derivation.derivationPath;
userKeychain.pub = derivation.key;
userKeychain.derivedFromParentWithSeed = params.coldDerivationSeed;
}
} else {
if (!canEncrypt) {
throw new Error('cannot generate user keypair without passphrase');
}
// Create the user key.
userKeychain = this.baseCoin.keychains().create();
userKeychain.encryptedPrv = this.bitgo.encrypt({ password: passphrase, input: userKeychain.prv });
userKeychainParams = {
pub: userKeychain.pub,
encryptedPrv: userKeychain.encryptedPrv,
originalPasscodeEncryptionCode: params.passcodeEncryptionCode,
};
}
userKeychainParams.reqId = reqId;
const newUserKeychain = await this.baseCoin.keychains().add(userKeychainParams);
return _.extend({}, newUserKeychain, userKeychain);
};
const backupKeychainPromise = async (): Promise<Keychain> => {
if (params.backupXpubProvider) {
// If requested, use a KRS or backup key provider
return this.baseCoin.keychains().createBackup({
provider: params.backupXpubProvider || 'defaultRMGBackupProvider',
disableKRSEmail: params.disableKRSEmail,
krsSpecific: params.krsSpecific,
type: this.baseCoin.getChain(),
passphrase: params.passphrase,
reqId,
});
}
// User provided backup xpub
if (params.backupXpub) {
// user provided backup ethereum address
return this.baseCoin.keychains().add({
pub: params.backupXpub,
source: 'backup',
reqId,
});
} else {
if (!canEncrypt) {
throw new Error('cannot generate backup keypair without passphrase');
}
// No provided backup xpub or address, so default to creating one here
return this.baseCoin.keychains().createBackup({ reqId, passphrase: params.passphrase });
}
};
const { userKeychain, backupKeychain, bitgoKeychain }: KeychainsTriplet = await promiseProps({
userKeychain: userKeychainPromise(),
backupKeychain: backupKeychainPromise(),
bitgoKeychain: this.baseCoin
.keychains()
.createBitGo({ enterprise: params.enterprise, reqId, isDistributedCustody: params.isDistributedCustody }),
});
walletParams.keys = [userKeychain.id, backupKeychain.id, bitgoKeychain.id];
const { prv } = userKeychain;
if (_.isString(prv)) {
assert(backupKeychain.pub);
assert(bitgoKeychain.pub);
walletParams.keySignatures = {
backup: (await this.baseCoin.signMessage({ prv }, backupKeychain.pub)).toString('hex'),
bitgo: (await this.baseCoin.signMessage({ prv }, bitgoKeychain.pub)).toString('hex'),
};
}
const keychains = {
userKeychain,
backupKeychain,
bitgoKeychain,
};
const finalWalletParams = await this.baseCoin.supplementGenerateWallet(walletParams, keychains);
if (_.includes(['xrp', 'xlm', 'cspr'], this.baseCoin.getFamily()) && !_.isUndefined(params.rootPrivateKey)) {
walletParams.rootPrivateKey = params.rootPrivateKey;
}
this.bitgo.setRequestTracer(reqId);
const newWallet = await this.bitgo.post(this.baseCoin.url('/wallet/add')).send(finalWalletParams).result();
const result: WalletWithKeychains = {
wallet: new Wallet(this.bitgo, this.baseCoin, newWallet),
userKeychain: userKeychain,
backupKeychain: backupKeychain,
bitgoKeychain: bitgoKeychain,
responseType: 'WalletWithKeychains',
};
if (!_.isUndefined(backupKeychain.prv)) {
result.warning = 'Be sure to backup the backup keychain -- it is not stored anywhere else!';
}
if (!_.isUndefined(derivationPath)) {
userKeychain.derivationPath = derivationPath;
}
if (canEncrypt && params.passcodeEncryptionCode) {
result.encryptedWalletPassphrase = this.bitgo.encrypt({
input: passphrase,
password: params.passcodeEncryptionCode,
});
}
return result;
}
}
/**
* List the user's wallet shares
* @param params
*/
async listShares(params: Record<string, unknown> = {}): Promise<any> {
return await this.bitgo.get(this.baseCoin.url('/walletshare')).result();
}
/**
* List the user's wallet shares v2
* @returns {Promise<WalletShares>}
*/
async listSharesV2(): Promise<WalletShares> {
return await this.bitgo.get(this.bitgo.url('/walletshares', 2)).result();
}
/**
* Gets a wallet share information, including the encrypted sharing keychain. requires unlock if keychain is present.
* @param params
* @param params.walletShareId - the wallet share to get information on
*/
async getShare(params: { walletShareId?: string } = {}): Promise<any> {
common.validateParams(params, ['walletShareId'], []);
return await this.bitgo.get(this.baseCoin.url('/walletshare/' + params.walletShareId)).result();
}
/**
* Update a wallet share
* @param params.walletShareId - the wallet share to update
* @param params.state - the new state of the wallet share
* @param params
*/
async updateShare(params: UpdateShareOptions = {}): Promise<any> {
common.validateParams(params, ['walletShareId'], []);
return await this.bitgo
.post(this.baseCoin.url('/walletshare/' + params.walletShareId))
.send(params)
.result();
}
/**
* Bulk accept wallet shares
* @param params AcceptShareOptionsRequest[]
* @returns {Promise<BulkAcceptShareResponse>}
*/
async bulkAcceptShareRequest(params: AcceptShareOptionsRequest[]): Promise<BulkAcceptShareResponse> {
return await this.bitgo
.put(this.bitgo.url('/walletshares/accept', 2))
.send({
keysForWalletShares: params,
})
.result();
}
async bulkUpdateWalletShareRequest(
params: BulkUpdateWalletShareOptionsRequest[]
): Promise<BulkUpdateWalletShareResponse> {
return await this.bitgo
.put(this.bitgo.url('/walletshares/update', 2))
.send({
shares: params,
})
.result();
}
/**
* Resend a wallet share invitation email
* @param params
* @param params.walletShareId - the wallet share whose invitiation should be resent
*/
async resendShareInvite(params: { walletShareId?: string } = {}): Promise<any> {
common.validateParams(params, ['walletShareId'], []);
const urlParts = params.walletShareId + '/resendemail';
return this.bitgo.post(this.baseCoin.url('/walletshare/' + urlParts)).result();
}
/**
* Cancel a wallet share
* @param params
* @param params.walletShareId - the wallet share to update
*/
async cancelShare(params: { walletShareId?: string } = {}): Promise<any> {
common.validateParams(params, ['walletShareId'], []);
return await this.bitgo
.del(this.baseCoin.url('/walletshare/' + params.walletShareId))
.send()
.result();
}
/**
* Re-share wallet with existing spenders of the wallet
* @param walletId
* @param userPassword
*/
async reshareWalletWithSpenders(walletId: string, userPassword: string): Promise<void> {
const wallet = await this.get({ id: walletId });
if (!wallet?._wallet?.enterprise) {
throw new Error('Enterprise not found for the wallet');
}
const enterpriseUsersResponse = await this.bitgo
.get(this.bitgo.url(`/enterprise/${wallet?._wallet?.enterprise}/user`))
.result();
// create a map of users for easy lookup - we need the user email id to share the wallet
const usersMap = new Map(
[...enterpriseUsersResponse?.adminUsers, ...enterpriseUsersResponse?.nonAdminUsers].map((obj) => [obj.id, obj])
);
if (wallet._wallet.users) {
for (const user of wallet._wallet.users) {
const userObject = usersMap.get(user.user);
if (user.permissions.includes('spend') && !user.permissions.includes('admin') && userObject) {
const shareParams = {
walletId: walletId,
user: user.user,
permissions: user.permissions.join(','),
walletPassphrase: userPassword,
email: userObject.email.email,
reshare: true,
skipKeychain: false,
};
await wallet.shareWallet(shareParams);
}
}
}
}
/**
* Accepts a wallet share, adding the wallet to the user's list
* Needs a user's password to decrypt the shared key
*
* @param params
* @param params.walletShareId - the wallet share to accept
* @param params.userPassword - (required if more a keychain was shared) user's password to decrypt the shared wallet
* @param params.newWalletPassphrase - new wallet passphrase for saving the shared wallet prv.
* If left blank and a wallet with more than view permissions was shared,
* then the user's login password is used.
* @param params.overrideEncryptedPrv - set only if the prv was received out-of-band.
*/
async acceptShare(params: AcceptShareOptions = {}): Promise<any> {
common.validateParams(params, ['walletShareId'], ['overrideEncryptedPrv', 'userPassword', 'newWalletPassphrase']);
let encryptedPrv = params.overrideEncryptedPrv;
const walletShare = await this.getShare({ walletShareId: params.walletShareId });
if (
walletShare.keychainOverrideRequired &&
walletShare.permissions.indexOf('admin') !== -1 &&
walletShare.permissions.indexOf('spend') !== -1
) {
if (_.isUndefined(params.userPassword)) {
throw new Error('userPassword param must be provided to decrypt shared key');
}
const walletKeychain = await this.baseCoin.keychains().createUserKeychain(params.userPassword);
if (_.isUndefined(walletKeychain.encryptedPrv)) {
throw new Error('encryptedPrv was not found on wallet keychain');
}
const payload = {
tradingAccountId: walletShare.wallet,
pubkey: walletKeychain.pub,
timestamp: new Date().toISOString(),
};
const payloadString = JSON.stringify(payload);
const privateKey = this.bitgo.decrypt({
password: params.userPassword,
input: walletKeychain.encryptedPrv,
});
const signature = await this.baseCoin.signMessage({ prv: privateKey }, payloadString);
const response = await this.updateShare({
walletShareId: params.walletShareId,
state: 'accepted',
keyId: walletKeychain.id,
signature: signature.toString('hex'),
payload: payloadString,
});
// If the wallet share was accepted successfully (changed=true), reshare the wallet with the spenders
if (response.changed && response.state === 'accepted') {
try {
await this.reshareWalletWithSpenders(walletShare.wallet, params.userPassword);
} catch (e) {
// TODO: PX-3826
// Do nothing
}
}
return response;
}
// Return right away if there is no keychain to decrypt, or if explicit encryptedPrv was provided
if (!walletShare.keychain || !walletShare.keychain.encryptedPrv || encryptedPrv) {
return this.updateShare({
walletShareId: params.walletShareId,
state: 'accepted',
});
}
// More than viewing was requested, so we need to process the wallet keys using the shared ecdh scheme
if (_.isUndefined(params.userPassword)) {
throw new Error('userPassword param must be provided to decrypt shared key');
}
const sharingKeychain = (await this.bitgo.getECDHKeychain()) as any;
if (_.isUndefined(sharingKeychain.encryptedXprv)) {
throw new Error('encryptedXprv was not found on sharing keychain');
}
// Now we have the sharing keychain, we can work out the secret used for sharing the wallet with us
sharingKeychain.prv = this.bitgo.decrypt({
password: params.userPassword,
input: sharingKeychain.encryptedXprv,
});
const secret = getSharedSecret(
// Derive key by path (which is used between these 2 users only)
bip32.fromBase58(sharingKeychain.prv).derivePath(sanitizeLegacyPath(walletShare.keychain.path)),
Buffer.from(walletShare.keychain.fromPubKey, 'hex')
).toString('hex');
// Yes! We got the secret successfully here, now decrypt the shared wallet prv
const decryptedSharedWalletPrv = this.bitgo.decrypt({
password: secret,
input: walletShare.keychain.encryptedPrv,
});
// We will now re-encrypt the wallet with our own password
const newWalletPassphrase = params.newWalletPassphrase || params.userPassword;
encryptedPrv = this.bitgo.encrypt({
password: newWalletPassphrase,
input: decryptedSharedWalletPrv,
});
const updateParams: UpdateShareOptions = {
walletShareId: params.walletShareId,
state: 'accepted',
};
if (encryptedPrv) {
updateParams.encryptedPrv = encryptedPrv;
}
return this.updateShare(updateParams);
}
/**
* Bulk Accept wallet shares, adding the wallets to the user's list
* Needs a user's password to decrypt the shared key
*
* @param params BulkAcceptShareOptions
* @param params.walletShareId - array of the wallet shares to accept
* @param params.userPassword - user's password to decrypt the shared wallet key
* @param params.newWalletPassphrase - new wallet passphrase for saving the shared wallet prv.
* If left blank then the user's login password is used.
*
*@returns {Promise<BulkAcceptShareResponse>}
*/
async bulkAcceptShare(params: BulkAcceptShareOptions): Promise<BulkAcceptShareResponse> {
common.validateParams(params, ['userLoginPassword'], ['newWalletPassphrase']);
assert(params.walletShareIds.length > 0, 'no walletShareIds are passed');
const allWalletShares = await this.listSharesV2();
const walletShareMap = allWalletShares.incoming.reduce(
(map: { [key: string]: WalletShare }, share) => ({ ...map, [share.id]: share }),
{}
);
const walletShares = params.walletShareIds
.map((walletShareId) => walletShareMap[walletShareId])
.filter((walletShare) => walletShare && walletShare.keychain);
if (!walletShares.length) {
throw new Error('invalid wallet shares provided');
}
const sharingKeychain = await this.bitgo.getECDHKeychain();
if (_.isUndefined(sharingKeychain.encryptedXprv)) {
throw new Error('encryptedXprv was not found on sharing keychain');
}
sharingKeychain.prv = this.bitgo.decrypt({
password: params.userLoginPassword,
input: sharingKeychain.encryptedXprv,
});
const newWalletPassphrase = params.newWalletPassphrase || params.userLoginPassword;
const keysForWalletShares = walletShares.flatMap((walletShare) => {
if (!walletShare.keychain) {
return [];
}
const secret = getSharedSecret(
bip32.fromBase58(sharingKeychain.prv).derivePath(sanitizeLegacyPath(walletShare.keychain.path)),
Buffer.from(walletShare.keychain.fromPubKey, 'hex')
).toString('hex');
const decryptedSharedWalletPrv = this.bitgo.decrypt({
password: secret,
input: walletShare.keychain.encryptedPrv,
});
const newEncryptedPrv = this.bitgo.encrypt({
password: newWalletPassphrase,
input: decryptedSharedWalletPrv,
});
return [
{
walletShareId: walletShare.id,
encryptedPrv: newEncryptedPrv,
},
];
});
return this.bulkAcceptShareRequest(keysForWalletShares);
}
/**
* Updates multiple wallet shares in bulk
* This method allows users to accept or reject multiple wallet shares in a single operation.
* It handles different types of wallet shares including those requiring special keychain overrides
* and those with encrypted private keys that need to be decrypted and re-encrypted.
* After processing, it also reshares accepted wallets with spenders for special override cases.
*
* @param params - Options for bulk updating wallet shares
* @param params.shares - Array of wallet shares to update with their status (accept/reject)
* @param params.userLoginPassword - User's login password for decryption operations
* @param params.newWalletPassphrase - New wallet passphrase for re-encryption
* @returns Array of responses for each wallet share update
*/
async bulkUpdateWalletShare(params: BulkUpdateWalletShareOptions): Promise<BulkUpdateWalletShareResponse> {
if (!params.shares) {
throw new Error('Missing parameter: shares');
}
if (!Array.isArray(params.shares)) {
throw new Error('Expecting parameter array: shares but found ' + typeof params.shares);
}
// Validate each share in the array
for (const share of params.shares) {
if (!share.walletShareId) {
throw new Error('Missing walletShareId in share');
}
if (!share.status) {
throw new Error('Missing status in share');
}
if (share.status !== 'accept' && share.status !== 'reject') {
throw new Error('Invalid status in share: ' + share.status + '. Must be either "accept" or "reject"');
}
if (typeof share.walletShareId !== 'string') {
throw new Error('Expecting walletShareId to be a string but found ' + typeof share.walletShareId);
}
}
// Validate optional parameters if provided
if (params.userLoginPassword !== undefined && typeof params.userLoginPassword !== 'string') {
throw new Error('Expecting parameter string: userLoginPassword but found ' + typeof params.userLoginPassword);
}
if (params.newWalletPassphrase !== undefined && typeof params.newWalletPassphrase !== 'string') {
throw new Error('Expecting parameter string: newWalletPassphrase but found ' + typeof params.newWalletPassphrase);
}
assert(params.shares.length > 0, 'no shares are passed');
const { shares: inputShares, userLoginPassword, newWalletPassphrase } = params;
const allWalletShares = await this.listSharesV2();
// Only include shares that are in the input array for efficiency
const shareIds = new Set(inputShares.map((share) => share.walletShareId));
const walletShareMap = new Map();
allWalletShares.incoming
.filter((share) => shareIds.has(share.id))
.forEach((share) => walletShareMap.set(share.id, share));
allWalletShares.outgoing
.filter((share) => shareIds.has(share.id))
.forEach((share) => walletShareMap.set(share.id, share));
const resolvedShares = inputShares.map((share) => {
const walletShare = walletShareMap.get(share.walletShareId);
if (!walletShare) {
throw new Error(`invalid wallet share provided: ${share.walletShareId}`);
}
return { ...share, walletShare };
});
// Identify special override cases that need resharing after acceptance
const specialOverrideCases = new Map();
resolvedShares.forEach((share) => {
if (
share.status === 'accept' &&
share.walletShare.keychainOverrideRequired &&
share.walletShare.permissions.includes('admin') &&
share.walletShare.permissions.includes('spend')
) {
specialOverrideCases.set(share.walletShareId, share.walletShare.wallet);
}
});
// Decrypt sharing keychain if needed (only once)
let sharingKeychainPrv: string | undefined;
// Only decrypt if there are shares to accept that might need it
const hasSharesRequiringDecryption =
specialOverrideCases.size > 0 ||
resolvedShares.some((share) => share.status === 'accept' && share.walletShare.keychain?.encryptedPrv);
if (userLoginPassword && hasSharesRequiringDecryption) {
const sharingKeychain = await this.bitgo.getECDHKeychain();
if (!sharingKeychain.encryptedXprv) {
throw new Error('encryptedXprv was not found on sharing keychain');
}
sharingKeychainPrv = this.bitgo.decrypt({
password: userLoginPassword,
input: sharingKeychain.encryptedXprv,
});
}
const settledUpdates = await Promise.allSettled(
resolvedShares.map(async (share) => {
const { walletShareId, status, walletShare } = share;
// Handle accept case
if (status === 'accept') {
return this.processAcceptShare(
walletShareId,
walletShare,
userLoginPassword,
newWalletPassphrase,
sharingKeychainPrv
);
}
// Handle reject case
return [
{
walletShareId,
status: 'reject' as const,
},
];
})
);
// Extract successful updates
const successfulUpdates = settledUpdates.flatMap((result) => (result.status === 'fulfilled' ? result.value : []));
// Extract failed updates - only from rejected promises
const failedUpdates = settledUpdates.reduce<Array<{ walletShareId: string; reason: string }>>(
(acc, result, index) => {
if (result.status === 'rejected') {
const rejectedResult = result;
acc.push({
walletShareId: resolvedShares[index].walletShareId,
reason: rejectedResult.reason?.message || String(rejectedResult.reason),
});
}
return acc;
},
[]
);
// Send successful updates to the server
const response = await this.bulkUpdateWalletShareRequest(successfulUpdates);
// Process accepted special override cases - reshare with spenders
if (response.acceptedWalletShares && response.acceptedWalletShares.length > 0 && userLoginPassword) {
// For each accepted wallet share that is a special override case, reshare with spenders
for (const walletShareId of response.acceptedWalletShares) {
if (specialOverrideCases.has(walletShareId)) {
const walletId = specialOverrideCases.get(walletShareId);
try {
await this.reshareWalletWithSpenders(walletId, userLoginPassword);
} catch (e) {
// Log error but continue processing other shares
console.error(`Error resharing wallet ${walletId} with spenders: ${e?.message}`);
}
}
}
}
// Add information about failed updates to the response
if (failedUpdates.length > 0) {
response.walletShareUpdateErrors.push(...failedUpdates);
}
return response;
}
/**
* Process a wallet share that is being accepted
* This method handles the different cases for accepting a wallet share:
* 1. Special override case requiring user keychain and signing
* 2. Simple case with no keychain to decrypt
* 3. Standard case requiring decryption and re-encryption
*
* @param walletShareId - ID of the wallet share
* @param walletShare - Wallet share object
* @param userLoginPassword - User's login password
* @param newWalletPassphrase - New wallet passphrase
* @param sharingKeychainPrv - Decrypted sharing keychain private key
* @returns Array of wallet share update requests
*/
private async processAcceptShare(
walletShareId: string,
walletShare: WalletShare,
userLoginPassword?: string,
newWalletPassphrase?: string,
sharingKeychainPrv?: string
): Promise<BulkUpdateWalletShareOptionsRequest[]> {
// Special override case: requires user keychain and signing
if (
walletShare.keychainOverrideRequired &&
walletShare.permissions.includes('admin') &&
walletShare.permissions.includes('spend')
) {
if (!userLoginPassword) {
throw new Error('userLoginPassword param must be provided to decrypt shared key');
}
const walletKeychain = await this.baseCoin.keychains().createUserKeychain(userLoginPassword);
if (!walletKeychain.encryptedPrv) {
throw new Error('encryptedPrv was not found on wallet keychain');
}
const payload = JSON.stringify({
tradingAccountId: walletShare.wallet,
pubkey: walletKeychain.pub,
timestamp: new Date().toISOString(),
});
const prv = this.bitgo.decrypt({
password: userLoginPassword,
input: walletKeychain.encryptedPrv,
});
const signature = await this.baseCoin.signMessage({ prv }, payload);
return [
{
walletShareId,
status: 'accept' as const,
keyId: walletKeychain.id,
signature: signature.toString('hex'),
payload,
},
];
}
// Return right away if there is no keychain to decrypt
if (!walletShare.keychain || !walletShare.keychain.encryptedPrv) {
return [
{
walletShareId,
status: 'accept' as const,
},
];
}
// More than viewing was requested, so we need to process the wallet keys using the shared ecdh scheme
if (!userLoginPassword) {
throw new Error('userLoginPassword param must be provided to decrypt shared key');
}
if (!sharingKeychainPrv) {
throw new Error('failed to retrieve and decrypt sharing keychain');
}
const derivedKey = bip32.fromBase58(sharingKeychainPrv).derivePath(sanitizeLegacyPath(walletShare.keychain.path));
const sharedSecret = getSharedSecret(derivedKey, Buffer.from(walletShare.keychain.fromPubKey, 'hex')).toString(
'hex'
);
const decryptedPrv = this.bitgo.decrypt({
password: sharedSecret,
input: walletShare.keychain.encryptedPrv,
});
// We will now re-encrypt the wallet with our own password
const encryptedPrv = this.bitgo.encrypt({
password: newWalletPassphrase || userLoginPassword,
input: decryptedPrv,
});
return [
{
walletShareId,
status: 'accept' as const,
encryptedPrv,
},
];
}
/**
* Get a wallet by its ID
* @param params
* @param params.id wallet id
* @returns {*}
*/
async getWallet(params: GetWalletOptions = {}): Promise<Wallet> {
common.validateParams(params, ['id'], []);
const query: GetWalletOptions = {};
if (params.allTokens) {
if (!_.isBoolean(params.allTokens)) {
throw new Error('invalid allTokens argument, expecting boolean');
}
query.allTokens = params.allTokens;
}
if (params.includeBalance !== undefined) {
query.includeBalance = params.includeBalance;
}
this.bitgo.setRequestTracer(params.reqId || new RequestTracer());
const wallet = await this.bitgo
.get(this.baseCoin.url('/wallet/' + params.id))
.query(query)
.result();
return new Wallet(this.bitgo, this.baseCoin, wallet);
}
/**
* Get a wallet by its address
* @param params
* @param params.address wallet address
* @returns {*}
*/
async getWalletByAddress(params: GetWalletByAddressOptions = {}): Promise<Wallet> {
common.validateParams(params, ['address'], []);
this.bitgo.setRequestTracer(params.reqId || new RequestTracer());
const wallet = await this.bitgo.get(this.baseCoin.url('/wallet/address/' + params.address)).result();
return new Wallet(this.bitgo, this.baseCoin, wallet);
}
/**
* For any given supported coin, get total balances for all wallets of that
* coin type on the account.
* @param params
* @returns {*}
*/
async getTotalBalances(params: Record<string, never> = {}): Promise<any> {
return await this.bitgo.get(this.baseCoin.url('/wallet/balances')).result();
}
/**
* Generates a TSS or BLS-DKG Wallet.
* @param params
* @private
*/
private async generateMpcWallet({
passphrase,
label,
multisigType,
enterprise,
walletVersion,
originalPasscodeEncryptionCode,
}: GenerateMpcWalletOptions): Promise<WalletWithKeychains> {
if (multisigType === 'tss' && this.baseCoin.getMPCAlgorithm() === 'ecdsa') {
const tssSettings: TssSettings = await this.bitgo
.get(this.bitgo.microservicesUrl('/api/v2/tss/settings'))
.result();
const multisigTypeVersion =
tssSettings.coinSettings[this.baseCoin.getFamily()]?.walletCreationSettings?.multiSigTypeVersion;
walletVersion = this.determineEcdsaMpcWalletVersion(walletVersion, multisigTypeVersion);
}
const reqId = new RequestTracer();
this.bitgo.setRequestTracer(reqId);
// Create MPC Keychains
const keychains = await this.baseCoin.keychains().createMpc({
multisigType,
passphrase,
enterprise,
originalPasscodeEncryptionCode,
});
// Create Wallet
const { userKeychain, backupKeychain, bitgoKeychain } = keychains;
const walletParams: SupplementGenerateWalletOptions = {
label,
m: 2,
n: 3,
keys: [userKeychain.id, backupKeychain.id, bitgoKeychain.id],
type: 'hot',
multisigType,
enterprise,
walletVersion,
};
const finalWalletParams = await this.baseCoin.supplementGenerateWallet(walletParams, keychains);
const newWallet = await this.bitgo.post(this.baseCoin.url('/wallet/add')).send(finalWalletParams).result();
const result: WalletWithKeychains = {
wallet: new Wallet(this.bitgo, this.baseCoin, newWallet),
userKeychain,
backupKeychain,
bitgoKeychain,
responseType: 'WalletWithKeychains',
};
if (!_.isUndefined(backupKeychain.prv)) {
result.warning = 'Be sure to backup the backup keychain -- it is not stored anywhere else!';
}
return result;
}
/**
* Generates a Self-Managed Cold TSS Wallet.
* @param params
* @private
*/
private async generateSMCMpcWallet({
label,
multisigType,
enterprise,
walletVersion,
bitgoKeyId,
commonKeychain,
coldDerivationSeed,
}: GenerateSMCMpcWalletOptions): Promise<WalletWithKeychains> {
const reqId = new RequestTracer();
this.bitgo.setRequestTracer(reqId);
let multisigTypeVersion: 'MPCv2' | undefined;
if (multisigType === 'tss' && this.baseCoin.getMPCAlgorithm() === 'ecdsa') {
const tssSettings: TssSettings = await this.bitgo
.get(this.bitgo.microservicesUrl('/api/v2/tss/settings'))
.result();
multisigTypeVersion =
tssSettings.coinSettings[this.baseCoin.getFamily()]?.walletCreationSettings?.coldMultiSigTypeVersion;
walletVersion = this.determineEcdsaMpcWalletVersion(walletVersion, multisigTypeVersion);
}
// Create MPC Keychains
const bitgoKeychain = await this.baseCoin.keychains().get({ id: bitgoKeyId });
if (!bitgoKeychain || !bitgoKeychain.commonKeychain) {
throw new Error('BitGo keychain not found');
}
if (bitgoKeychain.source !== 'bitgo') {
throw new Error('The provided bitgoKeyId is not a BitGo keychain');
}
if (bitgoKeychain.commonKeychain !== commonKeychain) {
throw new Error('The provided Common keychain mismatch with the provided Bitgo key');
}
if (!coldDerivationSeed) {
throw new Error('derivedFromParentWithSeed is required');
}
const userKeychainParams: AddKeychainOptions = {
source: 'user',
keyType: 'tss',
commonKeychain: commonKeychain,
derivedFromParentWithSeed: coldDerivationSeed,
isMPCv2: multisigTypeVersion === 'MPCv2' ? true : undefined,
};
const userKeychain = await this.baseCoin.keychains().add(userKeychainParams);
const backupKeyChainParams: AddKeychainOptions = {
source: 'backup',
keyType: 'tss',
commonKeychain: commonKeychain,
derivedFromParentWithSeed: coldDerivationSeed,
isMPCv2: multisigTypeVersion === 'MPCv2' ? true : undefined,
};
const backupKeychain = await this.baseCoin.keychains().add(backupKeyChainParams);
// Create Wallet
const keychains = { userKeychain, backupKeychain, bitgoKeychain };
const walletParams: SupplementGenerateWalletOptions = {
label,
m: 2,
n: 3,
keys: [userKeychain.id, backupKeychain.id, bitgoKeychain.id],
type: 'cold',
multisigType,
enterprise,
walletVersion,
};
const finalWalletParams = await this.baseCoin.supplementGenerateWallet(walletParams, keychains);
const newWallet = await this.bitgo.post(this.baseCoin.url('/wallet/add')).send(finalWalletParams).result();
const result: WalletWithKeychains = {
wallet: new Wallet(this.bitgo, this.baseCoin, newWallet),
userKeychain,
backupKeychain,
bitgoKeychain,
responseType: 'WalletWithKeychains',
};
return result;
}
/**
* Generates a Custodial TSS Wallet.
* @param params
* @private
*/
private async generateCustodialMpcWallet({
label,
multisigType,
enterprise,
walletVersion,
}: GenerateBaseMpcWalletOptions): Promise<WalletWithKeychains> {
const reqId = new RequestTracer();
this.bitgo.setRequestTracer(reqId);
if (multisigType === 'tss' && this.baseCoin.getMPCAlgorithm() === 'ecdsa') {
const tssSettings: TssSettings = await this.bitgo
.get(this.bitgo.microservicesUrl('/api/v2/tss/settings'))
.result();
const multisigTypeVersion =
tssSettings.coinSettings[this.baseCoin.getFamily()]?.walletCreationSettings?.custodialMultiSigTypeVersion;
walletVersion = this.determineEcdsaMpcWalletVersion(walletVersion, multisigTypeVersion);
}
const finalWalletParams = {
label,
multisigType,
enterprise,
walletVersion,
type: 'custodial',
};
// Create Wallet
const newWallet = await this.bitgo.post(this.baseCoin.url('/wallet/add')).send(finalWalletParams).result();
const wallet = new Wallet(this.bitgo, this.baseCoin, newWallet);
const keychains = wallet.keyIds();
const result: WalletWithKeychains = {
wallet,
userKeychain: { id: keychains[0], type: multisigType, source: 'user' },
backupKeychain: { id: keychains[1], type: multisigType, source: 'backup' },
bitgoKeychain: { id: keychains[2], type: multisigType, source: 'bitgo' },
responseType: 'WalletWithKeychains',
};
return result;
}
private determineEcdsaMpcWalletVersion(walletVersion?: number, multisigTypeVersion?: string): number | undefined {
if (this.baseCoin.isEVM() && multisigTypeVersion === 'MPCv2') {
if (!walletVersion || (walletVersion !== 5 && walletVersion !== 6)) {
return 5;
}
}
return walletVersion;
}
}
Выполнить команду
Для локальной разработки. Не используйте в интернете!