PHP WebShell
Текущая директория: /opt/BitGoJS/modules/abstract-utxo/src/recovery
Просмотр файла: crossChainRecovery.ts
import * as utxolib from '@bitgo/utxo-lib';
import { bip32, BIP32Interface } from '@bitgo/utxo-lib';
import { Dimensions } from '@bitgo/unspents';
import { BitGoBase, IWallet, Keychain, Triple, Wallet } from '@bitgo/sdk-core';
import { decrypt } from '@bitgo/sdk-api';
import { AbstractUtxoCoin, TransactionInfo } from '../abstractUtxoCoin';
import { signAndVerifyWalletTransaction } from '../sign';
const { unspentSum, scriptTypeForChain, outputScripts } = utxolib.bitgo;
type RootWalletKeys = utxolib.bitgo.RootWalletKeys;
type Unspent<TNumber extends number | bigint = number> = utxolib.bitgo.Unspent<TNumber>;
type WalletUnspent<TNumber extends number | bigint = number> = utxolib.bitgo.WalletUnspent<TNumber>;
type WalletUnspentLegacy<TNumber extends number | bigint = number> = utxolib.bitgo.WalletUnspentLegacy<TNumber>;
export interface BuildRecoveryTransactionOptions {
wallet: string;
faultyTxId: string;
recoveryAddress: string;
}
type FeeInfo = {
size: number;
feeRate: number;
fee: number;
payGoFee: number;
};
export interface CrossChainRecoveryUnsigned<TNumber extends number | bigint = number> {
txHex: string;
txInfo: TransactionInfo<TNumber>;
walletId: string;
feeInfo: FeeInfo;
address: string;
coin: string;
}
export interface CrossChainRecoverySigned<TNumber extends number | bigint = number> {
version: 1 | 2;
txHex: string;
txInfo: TransactionInfo<TNumber>;
walletId: string;
sourceCoin: string;
recoveryCoin: string;
recoveryAddress?: string;
recoveryAmount?: TNumber;
}
type WalletV1 = {
keychains: { xpub: string }[];
address({ address }: { address: string }): Promise<{ chain: number; index: number }>;
getEncryptedUserKeychain(): Promise<{ encryptedXprv: string }>;
};
export async function getWallet(
bitgo: BitGoBase,
coin: AbstractUtxoCoin,
walletId: string
): Promise<IWallet | WalletV1> {
try {
return await coin.wallets().get({ id: walletId });
} catch (e) {
// TODO: BG-46364 handle errors more gracefully
// The v2 endpoint coin.wallets().get() may throw 404 or 400 errors, but this should not prevent us from searching for the walletId in v1 wallets.
if (e.status >= 500) {
throw e;
}
}
try {
return await bitgo.wallets().get({ id: walletId });
} catch (e) {
throw new Error(`could not get wallet ${walletId} from v1 or v2: ${e.toString()}`);
}
}
/**
* @param recoveryCoin
* @param wallet
* @return wallet pubkeys
*/
export async function getWalletKeys(
recoveryCoin: AbstractUtxoCoin,
wallet: IWallet | WalletV1
): Promise<RootWalletKeys> {
let xpubs: Triple<string>;
if (wallet instanceof Wallet) {
const keychains = (await recoveryCoin.keychains().getKeysForSigning({ wallet })) as unknown as Keychain[];
if (keychains.length !== 3) {
throw new Error(`expected triple got ${keychains.length}`);
}
xpubs = keychains.map((k) => k.pub) as Triple<string>;
} else {
xpubs = (wallet as WalletV1).keychains.map((k) => k.xpub) as Triple<string>;
}
return new utxolib.bitgo.RootWalletKeys(xpubs.map((k) => bip32.fromBase58(k)) as Triple<BIP32Interface>);
}
export async function isWalletAddress(wallet: IWallet | WalletV1, address: string): Promise<boolean> {
try {
let addressData;
if (wallet instanceof Wallet) {
addressData = await wallet.getAddress({ address });
} else {
addressData = await (wallet as WalletV1).address({ address });
}
return addressData !== undefined;
} catch (e) {
return false;
}
}
/**
* @param coin
* @param txid
* @param amountType
* @param wallet
* @param apiKey - a blockchair api key
* @return all unspents for transaction outputs, including outputs from other transactions
*/
async function getAllRecoveryOutputs<TNumber extends number | bigint = number>(
coin: AbstractUtxoCoin,
txid: string,
amountType: 'number' | 'bigint' = 'number',
wallet: IWallet | WalletV1,
apiKey?: string
): Promise<Unspent<TNumber>[]> {
const api = coin.getRecoveryProvider(apiKey);
const tx = await api.getTransactionIO(txid);
const walletAddresses = (
await Promise.all(
tx.outputs.map(async (output) => {
// For some coins (bch) we need to convert the address to legacy format since the api returns the address
// in non legacy format. However, we want to keep the address in the same format as the response since we
// are going to hit the API again to fetch address unspents.
const canonicalAddress = coin.canonicalAddress(output.address);
const isWalletOwned = await isWalletAddress(wallet, canonicalAddress);
return isWalletOwned ? output.address : null;
})
)
).filter((address) => address !== null);
const unspents = await api.getUnspentsForAddresses(walletAddresses as string[]);
if (unspents.length === 0) {
throw new Error(`No recovery unspents found.`);
}
// the api may return cashaddr's instead of legacy for BCH and BCHA
// downstream processes's only expect legacy addresses
return unspents.map((recoveryOutput) => {
return {
...recoveryOutput,
address: coin.canonicalAddress(recoveryOutput.address),
value: utxolib.bitgo.toTNumber<TNumber>(BigInt(recoveryOutput.value), amountType),
};
});
}
/**
* Data required for address and signature derivation
*/
type ScriptId = {
chain: number;
index: number;
};
async function getScriptId(coin: AbstractUtxoCoin, wallet: IWallet | WalletV1, script: Buffer): Promise<ScriptId> {
const address = utxolib.address.fromOutputScript(script, coin.network);
let addressData: { chain: number; index: number };
if (wallet instanceof Wallet) {
addressData = await wallet.getAddress({ address });
} else {
addressData = await (wallet as WalletV1).address({ address });
}
if (typeof addressData.chain === 'number' && typeof addressData.index === 'number') {
return { chain: addressData.chain, index: addressData.index };
}
throw new Error(`invalid address data: ${JSON.stringify(addressData)}`);
}
/**
* Lookup address data from unspents on sourceCoin in address database of recoveryCoin.
* Return full walletUnspents including scriptId in sourceCoin format.
*
* @param sourceCoin
* @param recoveryCoin
* @param unspents
* @param wallet
* @return walletUnspents
*/
async function toWalletUnspents<TNumber extends number | bigint = number>(
sourceCoin: AbstractUtxoCoin,
recoveryCoin: AbstractUtxoCoin,
unspents: Unspent<TNumber>[],
wallet: IWallet | WalletV1
): Promise<WalletUnspent<TNumber>[]> {
const addresses = new Set(unspents.map((u) => u.address));
const walletUnspents: WalletUnspent<TNumber>[] = [];
for (const address of addresses) {
let scriptId;
try {
scriptId = await getScriptId(recoveryCoin, wallet, utxolib.address.toOutputScript(address, sourceCoin.network));
} catch (e) {
console.error(`error getting scriptId for ${address}:`, e);
continue;
}
const filteredUnspents = unspents
.filter((u) => u.address === address)
.map((u) => ({
...u,
...scriptId,
}));
walletUnspents.push(...filteredUnspents);
}
return walletUnspents;
}
/**
* @param coin
* @return feeRate for transaction
*/
async function getFeeRateSatVB(coin: AbstractUtxoCoin): Promise<number> {
// TODO: use feeRate API
const feeRate = {
bch: 20,
tbch: 20,
bcha: 20,
tbcha: 20,
bsv: 20,
tbsv: 20,
btc: 80,
tbtc: 80,
tbtcsig: 80,
tbtc4: 80,
tbtcbgsig: 80,
ltc: 100,
tltc: 100,
doge: 1000,
tdoge: 1000,
}[coin.getChain()];
if (!feeRate) {
throw new Error(`no feeRate for ${coin.getChain()}`);
}
return feeRate;
}
/**
* @param xprv
* @param passphrase
* @param wallet
* @return signing key
*/
async function getPrv(xprv?: string, passphrase?: string, wallet?: IWallet | WalletV1): Promise<BIP32Interface> {
if (xprv) {
const key = bip32.fromBase58(xprv);
if (key.isNeutered()) {
throw new Error(`not a private key`);
}
return key;
}
if (!wallet || !passphrase) {
throw new Error(`no xprv given: need wallet and passphrase to continue`);
}
let encryptedPrv: string;
if (wallet instanceof Wallet) {
encryptedPrv = (await wallet.getEncryptedUserKeychain()).encryptedPrv;
} else {
encryptedPrv = (await (wallet as WalletV1).getEncryptedUserKeychain()).encryptedXprv;
}
return getPrv(decrypt(passphrase, encryptedPrv));
}
/**
* @param network
* @param unspents
* @param targetAddress
* @param feeRateSatVB
* @param signer - if set, sign transaction
* @param amountType
* @return transaction spending full input amount to targetAddress
*/
function createSweepTransaction<TNumber extends number | bigint = number>(
network: utxolib.Network,
unspents: WalletUnspent<TNumber>[],
targetAddress: string,
feeRateSatVB: number,
signer?: utxolib.bitgo.WalletUnspentSigner<RootWalletKeys>,
amountType: 'number' | 'bigint' = 'number'
): utxolib.bitgo.UtxoTransaction<TNumber> {
const inputValue = unspentSum<TNumber>(unspents, amountType);
const vsize = Dimensions.fromUnspents(unspents, {
p2tr: { scriptPathLevel: 1 },
p2trMusig2: { scriptPathLevel: undefined },
})
.plus(Dimensions.fromOutput({ script: utxolib.address.toOutputScript(targetAddress, network) }))
.getVSize();
const fee = vsize * feeRateSatVB;
const transactionBuilder = utxolib.bitgo.createTransactionBuilderForNetwork<TNumber>(network);
transactionBuilder.addOutput(
targetAddress,
utxolib.bitgo.toTNumber<TNumber>(BigInt(inputValue) - BigInt(fee), amountType)
);
unspents.forEach((unspent) => {
utxolib.bitgo.addToTransactionBuilder(transactionBuilder, unspent);
});
let transaction = transactionBuilder.buildIncomplete();
if (signer) {
transaction = signAndVerifyWalletTransaction<TNumber>(transactionBuilder, unspents, signer, {
isLastSignature: false,
});
}
return transaction;
}
function getTxInfo<TNumber extends number | bigint = number>(
transaction: utxolib.bitgo.UtxoTransaction<TNumber>,
unspents: WalletUnspent<TNumber>[],
walletId: string,
walletKeys: RootWalletKeys,
amountType: 'number' | 'bigint' = 'number'
): TransactionInfo<TNumber> {
const inputAmount = utxolib.bitgo.unspentSum<TNumber>(unspents, amountType);
const outputAmount = utxolib.bitgo.toTNumber<TNumber>(
transaction.outs.reduce((sum, o) => sum + BigInt(o.value), BigInt(0)),
amountType
);
const outputs = transaction.outs.map((o) => ({
address: utxolib.address.fromOutputScript(o.script, transaction.network),
valueString: o.value.toString(),
change: false,
}));
const inputs = unspents.map((u) => {
// NOTE:
// The `redeemScript` and `walletScript` properties are required for legacy versions of BitGoJS
// which might require these scripts for signing. The Wallet Recovery Wizard (WRW) can create
// unsigned prebuilds that are submitted to BitGoJS instances which are not necessarily the same
// version.
const addressKeys = walletKeys.deriveForChainAndIndex(u.chain, u.index);
const scriptType = scriptTypeForChain(u.chain);
const { redeemScript, witnessScript } = outputScripts.createOutputScript2of3(addressKeys.publicKeys, scriptType);
return {
...u,
wallet: walletId,
fromWallet: walletId,
redeemScript: redeemScript?.toString('hex'),
witnessScript: witnessScript?.toString('hex'),
} as WalletUnspentLegacy<TNumber>;
});
return {
inputAmount,
outputAmount,
minerFee: inputAmount - outputAmount,
spendAmount: outputAmount,
inputs,
unspents: inputs,
outputs,
externalOutputs: outputs,
changeOutputs: [],
payGoFee: 0,
} /* cast to TransactionInfo to allow extra fields may be required by legacy consumers of this data */ as TransactionInfo<TNumber>;
}
function getFeeInfo<TNumber extends number | bigint = number>(
transaction: utxolib.bitgo.UtxoTransaction<TNumber>,
unspents: WalletUnspent<TNumber>[],
amountType: 'number' | 'bigint' = 'number'
): FeeInfo {
const vsize = Dimensions.fromUnspents(unspents, {
p2tr: { scriptPathLevel: 1 },
p2trMusig2: { scriptPathLevel: undefined },
})
.plus(Dimensions.fromOutputs(transaction.outs))
.getVSize();
const inputAmount = utxolib.bitgo.unspentSum<TNumber>(unspents, amountType);
const outputAmount = transaction.outs.reduce((sum, o) => sum + BigInt(o.value), BigInt(0));
const fee = Number(BigInt(inputAmount) - outputAmount);
return {
size: vsize,
fee,
feeRate: fee / vsize,
payGoFee: 0,
};
}
type RecoverParams = {
/** Wallet ID (can be v1 wallet or v2 wallet) */
walletId: string;
/** Coin to create the transaction for */
sourceCoin: AbstractUtxoCoin;
/** Coin that wallet keys were set up for */
recoveryCoin: AbstractUtxoCoin;
/** Source coin transaction to recover outputs from (sourceCoin) */
txid: string;
/** Source coin address to send the funds to */
recoveryAddress: string;
/** If set, decrypts private key and signs transaction */
walletPassphrase?: string;
/** If set, signs transaction */
xprv?: string;
/** for utxo coins other than [BTC,TBTC] this is a Block Chair api key **/
apiKey?: string;
};
/**
* Recover wallet deposits that were received on the wrong blockchain
* (for instance bitcoin deposits that were received for a litecoin wallet).
*
* Fetches the unspent data from BitGo's public blockchain API and the script data from the user's
* wallet.
*
* @param {BitGoBase} bitgo
* @param {RecoverParams} params
*/
export async function recoverCrossChain<TNumber extends number | bigint = number>(
bitgo: BitGoBase,
params: RecoverParams
): Promise<CrossChainRecoverySigned<TNumber> | CrossChainRecoveryUnsigned<TNumber>> {
const wallet = await getWallet(bitgo, params.recoveryCoin, params.walletId);
const unspents = await getAllRecoveryOutputs<TNumber>(
params.sourceCoin,
params.txid,
params.sourceCoin.amountType,
wallet,
params.apiKey
);
const walletUnspents = await toWalletUnspents<TNumber>(params.sourceCoin, params.recoveryCoin, unspents, wallet);
const walletKeys = await getWalletKeys(params.recoveryCoin, wallet);
const prv =
params.xprv || params.walletPassphrase ? await getPrv(params.xprv, params.walletPassphrase, wallet) : undefined;
const signer = prv
? new utxolib.bitgo.WalletUnspentSigner<RootWalletKeys>(walletKeys, prv, walletKeys.bitgo)
: undefined;
const feeRateSatVB = await getFeeRateSatVB(params.sourceCoin);
const transaction = createSweepTransaction<TNumber>(
params.sourceCoin.network,
walletUnspents,
params.recoveryAddress,
feeRateSatVB,
signer,
params.sourceCoin.amountType
);
const recoveryAmount = transaction.outs[0].value;
const txHex = transaction.toBuffer().toString('hex');
const txInfo = getTxInfo<TNumber>(
transaction,
walletUnspents,
params.walletId,
walletKeys,
params.sourceCoin.amountType
);
if (prv) {
return {
version: wallet instanceof Wallet ? 2 : 1,
walletId: params.walletId,
txHex,
txInfo,
sourceCoin: params.sourceCoin.getChain(),
recoveryCoin: params.recoveryCoin.getChain(),
recoveryAmount,
};
} else {
return {
txHex,
txInfo,
walletId: params.walletId,
feeInfo: getFeeInfo(transaction, walletUnspents, params.sourceCoin.amountType),
address: params.recoveryAddress,
coin: params.sourceCoin.getChain(),
};
}
}
Выполнить команду
Для локальной разработки. Не используйте в интернете!