PHP WebShell
Текущая директория: /opt/BitGoJS/modules/sdk-coin-ada/src
Просмотр файла: ada.ts
import assert from 'assert';
import {
BaseCoin,
BaseTransaction,
BitGoBase,
InvalidAddressError,
KeyPair,
MPCAlgorithm,
NodeEnvironmentError,
ParsedTransaction,
ParseTransactionOptions as BaseParseTransactionOptions,
SignedTransaction,
SignTransactionOptions as BaseSignTransactionOptions,
TransactionExplanation,
VerifyAddressOptions,
VerifyTransactionOptions,
EDDSAMethods,
EDDSAMethodTypes,
AddressFormat,
Environments,
ITransactionRecipient,
MPCTx,
MPCRecoveryOptions,
MPCConsolidationRecoveryOptions,
MPCSweepTxs,
RecoveryTxRequest,
MPCUnsignedTx,
MPCSweepRecoveryOptions,
MPCTxs,
PopulatedIntent,
PrebuildTransactionWithIntentOptions,
MultisigType,
multisigTypes,
} from '@bitgo/sdk-core';
import { KeyPair as AdaKeyPair, Transaction, TransactionBuilderFactory, Utils } from './lib';
import { BaseCoin as StaticsBaseCoin, CoinFamily, coins } from '@bitgo/statics';
import adaUtils from './lib/utils';
import * as request from 'superagent';
import BigNumber from 'bignumber.js';
import { getDerivationPath } from '@bitgo/sdk-lib-mpc';
export const DEFAULT_SCAN_FACTOR = 20; // default number of receive addresses to scan for funds
export interface TransactionPrebuild {
txHex: string;
}
export interface ExplainTransactionOptions {
txPrebuild: TransactionPrebuild;
}
export interface AdaParseTransactionOptions extends BaseParseTransactionOptions {
txPrebuild: TransactionPrebuild;
}
export interface SignTransactionOptions extends BaseSignTransactionOptions {
txPrebuild: TransactionPrebuild;
prv: string;
}
interface AdaAddressParams {
bitgoKey: string;
index: number;
seed?: string;
}
interface AdaAddressAndAccountId {
address: string;
accountId: string;
}
export type AdaTransactionExplanation = TransactionExplanation;
export class Ada extends BaseCoin {
protected readonly _staticsCoin: Readonly<StaticsBaseCoin>;
protected constructor(bitgo: BitGoBase, staticsCoin?: Readonly<StaticsBaseCoin>) {
super(bitgo);
if (!staticsCoin) {
throw new Error('missing required constructor parameter staticsCoin');
}
this._staticsCoin = staticsCoin;
}
static createInstance(bitgo: BitGoBase, staticsCoin?: Readonly<StaticsBaseCoin>): BaseCoin {
return new Ada(bitgo, staticsCoin);
}
/**
* Factor between the coin's base unit and its smallest subdivison
*/
public getBaseFactor(): number {
return 1e6;
}
public getChain(): string {
return this._staticsCoin.name;
}
public getFamily(): CoinFamily {
return this._staticsCoin.family;
}
public getFullName(): string {
return this._staticsCoin.fullName;
}
getBaseChain(): string {
return this.getChain();
}
/**
* Verify that a transaction prebuild complies with the original intention
* A prebuild transaction has to be parsed correctly and intended recipients has to be
* in the transaction output
*
* @param params.txPrebuild prebuild transaction
* @param params.txParams transaction parameters
* @return true if verification success
*
*/
async verifyTransaction(params: VerifyTransactionOptions): Promise<boolean> {
try {
const coinConfig = coins.get(this.getChain());
const { txPrebuild: txPrebuild, txParams: txParams } = params;
const transaction = new Transaction(coinConfig);
assert(txPrebuild.txHex, new Error('missing required tx prebuild property txHex'));
const rawTx = txPrebuild.txHex;
transaction.fromRawTransaction(rawTx);
const explainedTx = transaction.explainTransaction();
if (txParams.recipients !== undefined) {
for (const recipient of txParams.recipients) {
let find = false;
for (const output of explainedTx.outputs) {
if (recipient.address === output.address && recipient.amount === output.amount) {
find = true;
}
}
if (!find) {
throw new Error('cannot find recipient in expected output');
}
}
}
} catch (e) {
if (e instanceof NodeEnvironmentError) {
return true;
} else {
throw e;
}
}
return true;
}
async isWalletAddress(params: VerifyAddressOptions): Promise<boolean> {
const { address } = params;
if (!this.isValidAddress(address)) {
throw new InvalidAddressError(`Invalid Cardano Address: ${address}`);
}
return true;
}
/** @inheritDoc */
async signMessage(key: KeyPair, message: string | Buffer): Promise<Buffer> {
const adaKeypair = new AdaKeyPair({ prv: key.prv });
const messageHex = typeof message === 'string' ? message : message.toString('hex');
return Buffer.from(adaKeypair.signMessage(messageHex));
}
/**
* Explain/parse transaction
* @param params
*/
async explainTransaction(params: ExplainTransactionOptions): Promise<AdaTransactionExplanation> {
const factory = this.getBuilder();
let rebuiltTransaction: BaseTransaction;
const txRaw = params.txPrebuild.txHex;
try {
const transactionBuilder = factory.from(txRaw);
rebuiltTransaction = await transactionBuilder.build();
} catch {
throw new Error('Invalid transaction');
}
return rebuiltTransaction.explainTransaction();
}
async parseTransaction(params: AdaParseTransactionOptions): Promise<ParsedTransaction> {
const transactionExplanation = await this.explainTransaction({
txPrebuild: params.txPrebuild,
});
if (!transactionExplanation) {
throw new Error('Invalid transaction');
}
return transactionExplanation as unknown as ParsedTransaction;
}
generateKeyPair(seed?: Buffer): KeyPair {
const keyPair = seed ? new AdaKeyPair({ seed }) : new AdaKeyPair();
const keys = keyPair.getKeys();
if (!keys.prv) {
throw new Error('Missing prv in key generation.');
}
return {
pub: keys.pub,
prv: keys.prv,
};
}
isValidPub(pub: string): boolean {
return adaUtils.isValidPublicKey(pub);
}
isValidPrv(prv: string): boolean {
return adaUtils.isValidPrivateKey(prv);
}
isValidAddress(address: string): boolean {
return adaUtils.isValidAddress(address);
}
async signTransaction(params: SignTransactionOptions): Promise<SignedTransaction> {
const factory = this.getBuilder();
const txBuilder = factory.from(params.txPrebuild.txHex);
txBuilder.sign({ key: params.prv });
const transaction: BaseTransaction = await txBuilder.build();
if (!transaction) {
throw new Error('Invalid transaction');
}
const serializedTx = transaction.toBroadcastFormat();
return {
txHex: serializedTx,
};
}
protected getPublicNodeUrl(): string {
return Environments[this.bitgo.getEnv()].adaNodeUrl;
}
protected async getDataFromNode(endpoint: string, requestBody?: Record<string, unknown>): Promise<request.Response> {
const restEndpoint = this.getPublicNodeUrl() + '/' + endpoint;
try {
const res = await request.post(restEndpoint).send(requestBody);
return res;
} catch (e) {
console.debug(e);
}
throw new Error(`Unable to call endpoint ${restEndpoint}`);
}
protected async getAddressInfo(
walletAddr: string
): Promise<{ balance: number; utxoSet: Array<Record<string, any>> }> {
const requestBody = { _addresses: [walletAddr] };
const res = await this.getDataFromNode('address_info', requestBody);
if (res.status != 200) {
throw new Error(`Failed to retrieve address info for address ${walletAddr}`);
}
const body = res.body[0];
if (body === undefined) {
return { balance: 0, utxoSet: [] };
}
return { balance: body.balance, utxoSet: body.utxo_set };
}
protected async getChainTipInfo(): Promise<Record<string, string>> {
const res = await this.getDataFromNode('tip');
if (res.status != 200) {
throw new Error('Failed to retrieve chain tip info');
}
const body = res.body[0];
return body;
}
/** inherited doc */
async createBroadcastableSweepTransaction(params: MPCSweepRecoveryOptions): Promise<MPCTxs> {
const req = params.signatureShares;
const broadcastableTransactions: MPCTx[] = [];
let lastScanIndex = 0;
for (let i = 0; i < req.length; i++) {
const MPC = await EDDSAMethods.getInitializedMpcInstance();
const transaction = req[i].txRequest.transactions[0].unsignedTx;
if (!req[i].ovc || !req[i].ovc[0].eddsaSignature) {
throw new Error('Missing signature(s)');
}
const signature = req[i].ovc[0].eddsaSignature;
if (!transaction.signableHex) {
throw new Error('Missing signable hex');
}
const messageBuffer = Buffer.from(transaction.signableHex!, 'hex');
const result = MPC.verify(messageBuffer, signature);
if (!result) {
throw new Error('Invalid signature');
}
const signatureHex = Buffer.concat([Buffer.from(signature.R, 'hex'), Buffer.from(signature.sigma, 'hex')]);
const txBuilder = this.getBuilder().from(transaction.serializedTx as string);
if (!transaction.coinSpecific?.commonKeychain) {
throw new Error('Missing common keychain');
}
const commonKeychain = transaction.coinSpecific!.commonKeychain! as string;
if (!transaction.derivationPath) {
throw new Error('Missing derivation path');
}
const derivationPath = transaction.derivationPath as string;
const accountId = MPC.deriveUnhardened(commonKeychain, derivationPath).slice(0, 64);
const adaKeyPair = new AdaKeyPair({ pub: accountId });
// add combined signature from ovc
txBuilder.addSignature({ pub: adaKeyPair.getKeys().pub }, signatureHex);
const signedTransaction = await txBuilder.build();
const serializedTx = signedTransaction.toBroadcastFormat();
broadcastableTransactions.push({
serializedTx: serializedTx,
scanIndex: transaction.scanIndex,
});
if (i === req.length - 1 && transaction.coinSpecific!.lastScanIndex) {
lastScanIndex = transaction.coinSpecific!.lastScanIndex as number;
}
}
return { transactions: broadcastableTransactions, lastScanIndex };
}
/**
* Builds funds recovery transaction(s) without BitGo
*
* @param {MPCRecoveryOptions} params parameters needed to construct and
* (maybe) sign the transaction
*
* @returns {MPCTx | MPCSweepTxs} array of the serialized transaction hex strings and indices
* of the addresses being swept
*/
async recover(params: MPCRecoveryOptions): Promise<MPCTx | MPCSweepTxs> {
if (!params.bitgoKey) {
throw new Error('missing bitgoKey');
}
if (!params.recoveryDestination || !this.isValidAddress(params.recoveryDestination)) {
throw new Error('invalid recoveryDestination');
}
const index = params.index || 0;
const currPath = params.seed ? getDerivationPath(params.seed) + `/${index}` : `m/${index}`;
const bitgoKey = params.bitgoKey.replace(/\s/g, '');
const addressParams = {
bitgoKey: params.bitgoKey,
index: index,
seed: params.seed,
};
const { address: senderAddr, accountId } = await this.getAdaAddressAndAccountId(addressParams);
const isUnsignedSweep = !params.userKey && !params.backupKey && !params.walletPassphrase;
const { balance, utxoSet } = await this.getAddressInfo(senderAddr);
if (balance <= 0) {
throw new Error('Did not find address with funds to recover');
}
// first build the unsigned txn
const tipAbsSlot = await this.getChainTipInfo();
const txBuilder = this.getBuilder().getTransferBuilder();
txBuilder.changeAddress(params.recoveryDestination, balance.toString());
for (const utxo of utxoSet) {
txBuilder.input({ transaction_id: utxo.tx_hash, transaction_index: utxo.tx_index });
}
// each slot is about 1 second, so this transaction should be valid for
// 7 * 86,400 seconds (7 days) after creation
txBuilder.ttl(Number(tipAbsSlot.abs_slot) + 7 * 86400);
const unsignedTransaction = (await txBuilder.build()) as Transaction;
// sum up every output
const amount = unsignedTransaction
.toJson()
.outputs.reduce(
(acc: BigNumber, output: { amount: string }) => new BigNumber(acc).plus(output.amount),
new BigNumber(0)
);
if (amount.isLessThan(10000000)) {
throw new Error(
'Insufficient funds to recover, minimum required is 1 ADA plus fees, got ' +
amount.toString() +
' fees: ' +
unsignedTransaction.getFee
);
}
let serializedTx = unsignedTransaction.toBroadcastFormat();
if (!isUnsignedSweep) {
if (!params.userKey) {
throw new Error('missing userKey');
}
if (!params.backupKey) {
throw new Error('missing backupKey');
}
if (!params.walletPassphrase) {
throw new Error('missing wallet passphrase');
}
// Clean up whitespace from entered values
const userKey = params.userKey.replace(/\s/g, '');
const backupKey = params.backupKey.replace(/\s/g, '');
// Decrypt private keys from KeyCard values
let userPrv;
try {
userPrv = this.bitgo.decrypt({
input: userKey,
password: params.walletPassphrase,
});
} catch (e) {
throw new Error(`Error decrypting user keychain: ${e.message}`);
}
/** TODO BG-52419 Implement Codec for parsing */
const userSigningMaterial = JSON.parse(userPrv) as EDDSAMethodTypes.UserSigningMaterial;
let backupPrv;
try {
backupPrv = this.bitgo.decrypt({
input: backupKey,
password: params.walletPassphrase,
});
} catch (e) {
throw new Error(`Error decrypting backup keychain: ${e.message}`);
}
const backupSigningMaterial = JSON.parse(backupPrv) as EDDSAMethodTypes.BackupSigningMaterial;
// add signature
const signatureHex = await EDDSAMethods.getTSSSignature(
userSigningMaterial,
backupSigningMaterial,
currPath,
unsignedTransaction
);
const adaKeyPair = new AdaKeyPair({ pub: accountId });
txBuilder.addSignature({ pub: adaKeyPair.getKeys().pub }, signatureHex);
const signedTransaction = await txBuilder.build();
serializedTx = signedTransaction.toBroadcastFormat();
} else {
const transactionPrebuild = { txHex: serializedTx };
const parsedTx = await this.parseTransaction({ txPrebuild: transactionPrebuild });
const walletCoin = this.getChain();
const output = (parsedTx.outputs as ITransactionRecipient)[0];
const inputs = [
{
address: senderAddr,
valueString: output.amount,
value: new BigNumber(output.amount).toNumber(),
},
];
const outputs = [
{
address: output.address,
valueString: output.amount,
coinName: walletCoin,
},
];
const spendAmount = output.amount;
const completedParsedTx = { inputs: inputs, outputs: outputs, spendAmount: spendAmount, type: '' };
const fee = new BigNumber((parsedTx.fee as { fee: string }).fee);
const feeInfo = { fee: fee.toNumber(), feeString: fee.toString() };
const coinSpecific = { commonKeychain: bitgoKey };
const transaction: MPCTx = {
serializedTx: serializedTx,
scanIndex: index,
coin: walletCoin,
signableHex: unsignedTransaction.signablePayload.toString('hex'),
derivationPath: currPath,
parsedTx: completedParsedTx,
feeInfo: feeInfo,
coinSpecific: coinSpecific,
};
const unsignedTx: MPCUnsignedTx = { unsignedTx: transaction, signatureShares: [] };
const transactions: MPCUnsignedTx[] = [unsignedTx];
const txRequest: RecoveryTxRequest = {
transactions: transactions,
walletCoin: walletCoin,
};
const txRequests: MPCSweepTxs = { txRequests: [txRequest] };
return txRequests;
}
const transaction: MPCTx = { serializedTx: serializedTx, scanIndex: index };
return transaction;
}
/**
* Builds native ADA recoveries of receive addresses in batch without BitGo.
* Funds will be recovered to base address first. You need to initiate another sweep txn after that.
*
* @param {MPCConsolidationRecoveryOptions} params - options for consolidation recovery.
* @param {string} [params.startingScanIndex] - receive address index to start scanning from. default to 1 (inclusive).
* @param {string} [params.endingScanIndex] - receive address index to end scanning at. default to startingScanIndex + 20 (exclusive).
*/
async recoverConsolidations(params: MPCConsolidationRecoveryOptions): Promise<MPCTxs | MPCSweepTxs> {
const isUnsignedSweep = !params.userKey && !params.backupKey && !params.walletPassphrase;
const startIdx = params.startingScanIndex || 1;
const endIdx = params.endingScanIndex || startIdx + DEFAULT_SCAN_FACTOR;
if (startIdx < 1 || endIdx <= startIdx || endIdx - startIdx > 10 * DEFAULT_SCAN_FACTOR) {
throw new Error(
`Invalid starting or ending index to scan for addresses. startingScanIndex: ${startIdx}, endingScanIndex: ${endIdx}.`
);
}
const addressParams = {
bitgoKey: params.bitgoKey,
index: 0,
seed: params.seed,
};
const { address: baseAddress } = await this.getAdaAddressAndAccountId(addressParams);
const consolidationTransactions: any[] = [];
let lastScanIndex = startIdx;
for (let i = startIdx; i < endIdx; i++) {
const recoverParams = {
userKey: params.userKey,
backupKey: params.backupKey,
bitgoKey: params.bitgoKey,
walletPassphrase: params.walletPassphrase,
recoveryDestination: baseAddress,
seed: params.seed,
index: i,
};
let recoveryTransaction;
try {
recoveryTransaction = await this.recover(recoverParams);
} catch (e) {
if (e.message === 'Did not find address with funds to recover') {
lastScanIndex = i;
continue;
}
throw e;
}
if (isUnsignedSweep) {
consolidationTransactions.push((recoveryTransaction as MPCSweepTxs).txRequests[0]);
} else {
consolidationTransactions.push(recoveryTransaction);
}
lastScanIndex = i;
}
if (consolidationTransactions.length == 0) {
throw new Error('Did not find an address with funds to recover');
}
if (isUnsignedSweep) {
// lastScanIndex will be used to inform user the last address index scanned for available funds (so they can
// appropriately adjust the scan range on the next iteration of consolidation recoveries). In the case of unsigned
// sweep consolidations, this lastScanIndex will be provided in the coinSpecific of the last txn made.
const lastTransactionCoinSpecific = {
commonKeychain:
consolidationTransactions[consolidationTransactions.length - 1].transactions[0].unsignedTx.coinSpecific
.commonKeychain,
lastScanIndex: lastScanIndex,
};
consolidationTransactions[consolidationTransactions.length - 1].transactions[0].unsignedTx.coinSpecific =
lastTransactionCoinSpecific;
const consolidationSweepTransactions: MPCSweepTxs = { txRequests: consolidationTransactions };
return consolidationSweepTransactions;
}
return { transactions: consolidationTransactions, lastScanIndex };
}
/**
* Obtains ADA address and account id from provided bitgo key for the given index and seed (optional).
*
* @param {AdaAddressParams} params - params to obtain ada address and account id
*/
async getAdaAddressAndAccountId(params: AdaAddressParams): Promise<AdaAddressAndAccountId> {
if (!params.bitgoKey) {
throw new Error('missing bitgoKey');
}
let addrFormat = AddressFormat.testnet;
if (this.getChain() === 'ada') {
addrFormat = AddressFormat.mainnet;
}
const bitgoKey = params.bitgoKey.replace(/\s/g, '');
const MPC = await EDDSAMethods.getInitializedMpcInstance();
const derivationPathPrefix = params.seed ? getDerivationPath(params.seed) : 'm';
const stakeKeyPair = new AdaKeyPair({
pub: MPC.deriveUnhardened(bitgoKey, derivationPathPrefix + '/0').slice(0, 64),
});
const currPath = derivationPathPrefix + `/${params.index}`;
const accountId = MPC.deriveUnhardened(bitgoKey, currPath).slice(0, 64);
const paymentKeyPair = new AdaKeyPair({ pub: accountId });
const address = Utils.default.createBaseAddressWithStakeAndPaymentKey(stakeKeyPair, paymentKeyPair, addrFormat);
return { address, accountId };
}
/** inherited doc */
supportsTss(): boolean {
return true;
}
/** inherited doc */
getDefaultMultisigType(): MultisigType {
return multisigTypes.tss;
}
/** inherited doc */
getMPCAlgorithm(): MPCAlgorithm {
return 'eddsa';
}
/** inherited doc */
allowsAccountConsolidations(): boolean {
return true;
}
private getBuilder(): TransactionBuilderFactory {
return new TransactionBuilderFactory(coins.get(this.getBaseChain()));
}
/** inherited doc */
setCoinSpecificFieldsInIntent(intent: PopulatedIntent, params: PrebuildTransactionWithIntentOptions): void {
intent.unspents = params.unspents;
intent.senderAddress = params.senderAddress;
}
}
Выполнить команду
Для локальной разработки. Не используйте в интернете!