PHP WebShell
Текущая директория: /opt/BitGoJS/modules/sdk-coin-ada/src/lib
Просмотр файла: transactionBuilder.ts
import BigNumber from 'bignumber.js';
import { BaseCoin as CoinConfig } from '@bitgo/statics';
import {
BaseAddress,
BaseKey,
BaseTransactionBuilder,
BuildTransactionError,
PublicKey as BasePublicKey,
Signature,
TransactionType,
UtilsError,
} from '@bitgo/sdk-core';
import { Asset, Transaction, TransactionInput, TransactionOutput, Withdrawal } from './transaction';
import { KeyPair } from './keyPair';
import util, { MIN_ADA_FOR_ONE_ASSET } from './utils';
import * as CardanoWasm from '@emurgo/cardano-serialization-lib-nodejs';
import { BigNum } from '@emurgo/cardano-serialization-lib-nodejs';
export abstract class TransactionBuilder extends BaseTransactionBuilder {
protected _transaction!: Transaction;
protected _signers: KeyPair[] = [];
protected _transactionInputs: TransactionInput[] = [];
protected _transactionOutputs: TransactionOutput[] = [];
protected _initSignatures: Signature[] = [];
protected _signatures: Signature[] = [];
protected _changeAddress: string;
protected _senderBalance: string;
protected _ttl = 0;
protected _certs: CardanoWasm.Certificate[] = [];
protected _withdrawals: Withdrawal[] = [];
protected _type: TransactionType;
protected _multiAssets: Asset[] = [];
private _fee: BigNum;
constructor(_coinConfig: Readonly<CoinConfig>) {
super(_coinConfig);
this.transaction = new Transaction(_coinConfig);
this._fee = BigNum.zero();
}
input(i: TransactionInput): this {
this._transactionInputs.push(i);
return this;
}
output(o: TransactionOutput): this {
this._transactionOutputs.push(o);
return this;
}
assets(a: Asset): this {
this._multiAssets.push(a);
return this;
}
ttl(t: number): this {
this._ttl = t;
return this;
}
changeAddress(addr: string, totalInputBalance: string): this {
this._changeAddress = addr;
this._senderBalance = totalInputBalance;
return this;
}
fee(fee: string): this {
this._fee = BigNum.from_str(fee);
return this;
}
/**
* Initialize the transaction builder fields using the decoded transaction data
*
* @param {Transaction} tx the transaction data
*/
initBuilder(tx: Transaction): void {
this._transaction = tx;
const txnBody = tx.transaction.body();
for (let i = 0; i < txnBody.inputs().len(); i++) {
const input = txnBody.inputs().get(i);
this.input({
transaction_id: Buffer.from(input.transaction_id().to_bytes()).toString('hex'),
transaction_index: input.index(),
});
}
for (let i = 0; i < txnBody.outputs().len(); i++) {
const output = txnBody.outputs().get(i);
this.output({
address: output.address().to_bech32(),
amount: output.amount().coin().to_str(),
multiAssets: output.amount().multiasset() || undefined,
});
}
if (txnBody.certs() !== undefined) {
const certs = txnBody.certs() as CardanoWasm.Certificates;
for (let i = 0; i < certs.len(); i++) {
this._certs.push(certs.get(i));
}
}
if (txnBody.withdrawals() !== undefined) {
const withdrawals = txnBody.withdrawals() as CardanoWasm.Withdrawals;
const keys = withdrawals.keys();
for (let i = 0; i < keys.len(); i++) {
const rewardAddress = keys.get(i) as CardanoWasm.RewardAddress;
const reward = withdrawals.get(rewardAddress) as CardanoWasm.BigNum;
this._withdrawals.push({ stakeAddress: rewardAddress.to_address().to_bech32(), value: reward.to_str() });
}
}
this._ttl = tx.transaction.body().ttl() as number;
this._fee = tx.transaction.body().fee();
if (tx.transaction.witness_set().vkeys()) {
const vkeys = tx.transaction.witness_set().vkeys()! as CardanoWasm.Vkeywitnesses;
for (let i = 0; i < vkeys.len(); i++) {
const vkey = vkeys.get(i);
this._initSignatures.push({
publicKey: { pub: vkey.vkey().public_key().to_hex() },
signature: Buffer.from(vkey.signature().to_hex(), 'hex'),
});
}
}
}
/** @inheritdoc */
protected fromImplementation(rawTransaction: string): Transaction {
this.validateRawTransaction(rawTransaction);
this.buildImplementation();
return this.transaction;
}
/** @inheritdoc */
protected async buildImplementation(): Promise<Transaction> {
const inputs = CardanoWasm.TransactionInputs.new();
this._transactionInputs.forEach((input) => {
inputs.add(
CardanoWasm.TransactionInput.new(
CardanoWasm.TransactionHash.from_bytes(Buffer.from(input.transaction_id, 'hex')),
input.transaction_index
)
);
});
let outputs = CardanoWasm.TransactionOutputs.new();
let totalAmountToSend = CardanoWasm.BigNum.zero();
this._transactionOutputs.forEach((output) => {
const amount = CardanoWasm.BigNum.from_str(output.amount);
outputs.add(
CardanoWasm.TransactionOutput.new(util.getWalletAddress(output.address), CardanoWasm.Value.new(amount))
);
totalAmountToSend = totalAmountToSend.checked_add(amount);
});
if (this._fee.is_zero()) {
// estimate fee
// add extra output for the change
if (this._changeAddress && this._senderBalance) {
const changeAddress = util.getWalletAddress(this._changeAddress);
const utxoBalance = CardanoWasm.BigNum.from_str(this._senderBalance);
const adjustment = BigNum.from_str('2000000');
let change = utxoBalance.checked_sub(this._fee).checked_sub(totalAmountToSend);
if (this._type === TransactionType.StakingActivate) {
change = change.checked_sub(adjustment);
} else if (this._type === TransactionType.StakingDeactivate) {
change = change.checked_add(adjustment);
} else if (this._type === TransactionType.StakingWithdraw || this._type === TransactionType.StakingClaim) {
this._withdrawals.forEach((withdrawal: Withdrawal) => {
change = change.checked_add(CardanoWasm.BigNum.from_str(withdrawal.value));
});
}
// If totalAmountToSend is 0, its consolidation
if (totalAmountToSend.to_str() == '0') {
// support for multi-asset consolidation
if (this._multiAssets !== undefined) {
const totalNumberOfAssets = CardanoWasm.BigNum.from_str(this._multiAssets.length.toString());
const minAmountNeededForOneAssetOutput = CardanoWasm.BigNum.from_str(MIN_ADA_FOR_ONE_ASSET);
const minAmountNeededForTotalAssetOutputs =
minAmountNeededForOneAssetOutput.checked_mul(totalNumberOfAssets);
if (!change.less_than(minAmountNeededForTotalAssetOutputs)) {
this._multiAssets.forEach((asset) => {
let txOutputBuilder = CardanoWasm.TransactionOutputBuilder.new();
// changeAddress is the root address, which is where we want the tokens assets to be sent to
const toAddress = util.getWalletAddress(this._changeAddress);
txOutputBuilder = txOutputBuilder.with_address(toAddress);
let txOutputAmountBuilder = txOutputBuilder.next();
const assetName = CardanoWasm.AssetName.new(Buffer.from(asset.asset_name, 'hex'));
const policyId = CardanoWasm.ScriptHash.from_bytes(Buffer.from(asset.policy_id, 'hex'));
const multiAsset = CardanoWasm.MultiAsset.new();
const assets = CardanoWasm.Assets.new();
assets.insert(assetName, CardanoWasm.BigNum.from_str(asset.quantity));
multiAsset.insert(policyId, assets);
txOutputAmountBuilder = txOutputAmountBuilder.with_coin_and_asset(
minAmountNeededForOneAssetOutput,
multiAsset
);
const txOutput = txOutputAmountBuilder.build();
outputs.add(txOutput);
});
// finally send the remaining ADA in its own output
const remainingOutputAmount = change.checked_sub(minAmountNeededForTotalAssetOutputs);
const changeOutput = CardanoWasm.TransactionOutput.new(
changeAddress,
CardanoWasm.Value.new(remainingOutputAmount)
);
outputs.add(changeOutput);
}
} else {
// If there are no tokens to consolidate, you only have 1 output which is ADA alone
const changeOutput = CardanoWasm.TransactionOutput.new(changeAddress, CardanoWasm.Value.new(change));
outputs.add(changeOutput);
}
} else {
// If this isn't a consolidate request, whatever change that needs to be sent back to the rootaddress is added as a separate output here
const changeOutput = CardanoWasm.TransactionOutput.new(changeAddress, CardanoWasm.Value.new(change));
outputs.add(changeOutput);
}
}
const txBody = CardanoWasm.TransactionBody.new_tx_body(inputs, outputs, this._fee);
txBody.set_ttl(CardanoWasm.BigNum.from_str(this._ttl.toString()));
const txHash = CardanoWasm.hash_transaction(txBody);
// we add witnesses once so that we can get the appropriate amount of signers for calculating the fee
const witnessSet = CardanoWasm.TransactionWitnessSet.new();
const vkeyWitnesses = CardanoWasm.Vkeywitnesses.new();
this._signers.forEach((keyPair) => {
const prv = keyPair.getKeys().prv as string;
const vkeyWitness = CardanoWasm.make_vkey_witness(
txHash,
CardanoWasm.PrivateKey.from_normal_bytes(Buffer.from(prv, 'hex'))
);
vkeyWitnesses.add(vkeyWitness);
});
this.getAllSignatures().forEach((signature) => {
const vkey = CardanoWasm.Vkey.new(
CardanoWasm.PublicKey.from_bytes(Buffer.from(signature.publicKey.pub, 'hex'))
);
const ed255Sig = CardanoWasm.Ed25519Signature.from_bytes(signature.signature);
vkeyWitnesses.add(CardanoWasm.Vkeywitness.new(vkey, ed255Sig));
});
if (vkeyWitnesses.len() === 0) {
const prv = CardanoWasm.PrivateKey.generate_ed25519();
const vkeyWitness = CardanoWasm.make_vkey_witness(txHash, prv);
vkeyWitnesses.add(vkeyWitness);
if (this._type !== TransactionType.Send) {
vkeyWitnesses.add(vkeyWitness);
}
}
witnessSet.set_vkeys(vkeyWitnesses);
// add in withdrawal if this is a withdrawal tx
if (this._withdrawals.length > 0) {
const withdrawals = CardanoWasm.Withdrawals.new();
this._withdrawals.forEach((withdrawal: Withdrawal) => {
const rewardAddress = CardanoWasm.RewardAddress.from_address(
CardanoWasm.Address.from_bech32(withdrawal.stakeAddress)
);
withdrawals.insert(rewardAddress!, CardanoWasm.BigNum.from_str(withdrawal.value));
});
txBody.set_withdrawals(withdrawals);
}
// add in certificates to get mock size
const draftCerts = CardanoWasm.Certificates.new();
for (const cert of this._certs) {
draftCerts.add(cert);
}
txBody.set_certs(draftCerts);
const txDraft = CardanoWasm.Transaction.new(txBody, witnessSet);
const linearFee = CardanoWasm.LinearFee.new(
CardanoWasm.BigNum.from_str('44'),
CardanoWasm.BigNum.from_str('155381')
);
// calculate the fee based off our dummy transaction
const fee = CardanoWasm.min_fee(txDraft, linearFee).checked_add(BigNum.from_str('440'));
this._fee = fee;
}
this._transaction.fee(this._fee.to_str());
// now calculate the change based off of <utxoBalance> - <fee> - <amountToSend>
// reset the outputs collection because now our last output has changed
outputs = CardanoWasm.TransactionOutputs.new();
this._transactionOutputs.forEach((output) => {
if (output.multiAssets) {
const policyId = output.multiAssets.keys().get(0);
const assets = output.multiAssets.get(policyId);
const assetName = assets!.keys().get(0);
const quantity = assets!.get(assetName);
let txOutputBuilder = CardanoWasm.TransactionOutputBuilder.new();
const outputAmount = CardanoWasm.BigNum.from_str(output.amount);
const toAddress = util.getWalletAddress(output.address);
txOutputBuilder = txOutputBuilder.with_address(toAddress);
let txOutputAmountBuilder = txOutputBuilder.next();
const multiAsset = CardanoWasm.MultiAsset.new();
const asset = CardanoWasm.Assets.new();
asset.insert(assetName, quantity!);
multiAsset.insert(policyId, asset);
txOutputAmountBuilder = txOutputAmountBuilder.with_coin_and_asset(outputAmount, multiAsset);
const txOutput = txOutputAmountBuilder.build();
outputs.add(txOutput);
} else {
outputs.add(
CardanoWasm.TransactionOutput.new(
util.getWalletAddress(output.address),
CardanoWasm.Value.new(CardanoWasm.BigNum.from_str(output.amount))
)
);
}
});
if (this._changeAddress && this._senderBalance) {
const changeAddress = util.getWalletAddress(this._changeAddress);
const utxoBalance = CardanoWasm.BigNum.from_str(this._senderBalance);
const adjustment = BigNum.from_str('2000000');
let change = utxoBalance.checked_sub(this._fee).checked_sub(totalAmountToSend);
if (this._type === TransactionType.StakingActivate) {
change = change.checked_sub(adjustment);
} else if (this._type === TransactionType.StakingDeactivate) {
change = change.checked_add(adjustment);
} else if (this._type === TransactionType.StakingWithdraw || this._type === TransactionType.StakingClaim) {
this._withdrawals.forEach((withdrawal: Withdrawal) => {
change = change.checked_add(CardanoWasm.BigNum.from_str(withdrawal.value));
});
}
// If totalAmountToSend is 0, its consolidation
if (totalAmountToSend.to_str() == '0') {
// support for multi-asset consolidation
if (this._multiAssets !== undefined) {
const totalNumberOfAssets = CardanoWasm.BigNum.from_str(this._multiAssets.length.toString());
const minAmountNeededForOneAssetOutput = CardanoWasm.BigNum.from_str('1500000');
const minAmountNeededForTotalAssetOutputs = minAmountNeededForOneAssetOutput.checked_mul(totalNumberOfAssets);
if (!change.less_than(minAmountNeededForTotalAssetOutputs)) {
this._multiAssets.forEach((asset) => {
let txOutputBuilder = CardanoWasm.TransactionOutputBuilder.new();
// changeAddress is the root address, which is where we want the tokens assets to be sent to
const toAddress = util.getWalletAddress(this._changeAddress);
txOutputBuilder = txOutputBuilder.with_address(toAddress);
let txOutputAmountBuilder = txOutputBuilder.next();
const assetName = CardanoWasm.AssetName.new(Buffer.from(asset.asset_name, 'hex'));
const policyId = CardanoWasm.ScriptHash.from_bytes(Buffer.from(asset.policy_id, 'hex'));
const multiAsset = CardanoWasm.MultiAsset.new();
const assets = CardanoWasm.Assets.new();
assets.insert(assetName, CardanoWasm.BigNum.from_str(asset.quantity));
multiAsset.insert(policyId, assets);
txOutputAmountBuilder = txOutputAmountBuilder.with_coin_and_asset(
minAmountNeededForOneAssetOutput,
multiAsset
);
const txOutput = txOutputAmountBuilder.build();
outputs.add(txOutput);
});
// finally send the remaining ADA in its own output
const remainingOutputAmount = change.checked_sub(minAmountNeededForTotalAssetOutputs);
const changeOutput = CardanoWasm.TransactionOutput.new(
changeAddress,
CardanoWasm.Value.new(remainingOutputAmount)
);
outputs.add(changeOutput);
} else {
throw new BuildTransactionError(
'Insufficient funds: need a minimum of 1.5 ADA per output to construct token consolidation'
);
}
} else {
// If there are no tokens to consolidate, you only have 1 output which is ADA alone
const changeOutput = CardanoWasm.TransactionOutput.new(changeAddress, CardanoWasm.Value.new(change));
outputs.add(changeOutput);
}
} else {
// If this isn't a consolidate request, whatever change that needs to be sent back to the rootaddress is added as a separate output here
const changeOutput = CardanoWasm.TransactionOutput.new(changeAddress, CardanoWasm.Value.new(change));
outputs.add(changeOutput);
}
}
const txRaw = CardanoWasm.TransactionBody.new_tx_body(inputs, outputs, this._fee);
const certs = CardanoWasm.Certificates.new();
for (const cert of this._certs) {
certs.add(cert);
}
txRaw.set_certs(certs);
// add in withdrawal if this is a withdrawal tx
if (this._withdrawals.length > 0) {
const withdrawals = CardanoWasm.Withdrawals.new();
this._withdrawals.forEach((withdrawal: Withdrawal) => {
const rewardAddress = CardanoWasm.RewardAddress.from_address(
CardanoWasm.Address.from_bech32(withdrawal.stakeAddress)
);
withdrawals.insert(rewardAddress!, CardanoWasm.BigNum.from_str(withdrawal.value));
});
txRaw.set_withdrawals(withdrawals);
}
txRaw.set_ttl(CardanoWasm.BigNum.from_str(this._ttl.toString()));
const txRawHash = CardanoWasm.hash_transaction(txRaw);
// now add the witnesses again this time for real. We need to do this again
// because now that we've added our real fee and change output, we have a difference transaction hash
const witnessSet = CardanoWasm.TransactionWitnessSet.new();
const vkeyWitnesses = CardanoWasm.Vkeywitnesses.new();
this._signers.forEach((keyPair) => {
const prv = keyPair.getKeys().prv as string;
const vkeyWitness = CardanoWasm.make_vkey_witness(
txRawHash,
CardanoWasm.PrivateKey.from_normal_bytes(Buffer.from(prv, 'hex'))
);
vkeyWitnesses.add(vkeyWitness);
});
// Clear the cosmetic signature array in native txn wrapper to prevent duplicate when builder is inited from a partially witnessed txn
this._transaction.signature.length = 0;
this.getAllSignatures().forEach((signature) => {
const vkey = CardanoWasm.Vkey.new(CardanoWasm.PublicKey.from_bytes(Buffer.from(signature.publicKey.pub, 'hex')));
const ed255Sig = CardanoWasm.Ed25519Signature.from_bytes(signature.signature);
vkeyWitnesses.add(CardanoWasm.Vkeywitness.new(vkey, ed255Sig));
this._transaction.signature.push(signature.signature.toString('hex'));
});
witnessSet.set_vkeys(vkeyWitnesses);
this._transaction.transaction = CardanoWasm.Transaction.new(txRaw, witnessSet);
return this.transaction;
}
/** @inheritdoc */
protected signImplementation(key: BaseKey): Transaction {
this._signers.push(new KeyPair({ prv: key.key }));
return this._transaction;
}
/** @inheritdoc */
protected get transaction(): Transaction {
return this._transaction;
}
/** @inheritdoc */
protected set transaction(transaction: Transaction) {
this._transaction = transaction;
}
/** @inheritdoc */
validateAddress(address: BaseAddress, addressFormat?: string): void {
if (!util.isValidAddress(address.address)) {
throw new UtilsError('invalid address ' + address.address);
}
}
/** @inheritdoc */
validateKey(key: BaseKey): void {
try {
new KeyPair({ prv: key.key });
} catch {
throw new BuildTransactionError(`Key validation failed`);
}
}
/** @inheritdoc */
validateRawTransaction(rawTransaction: any): void {
try {
CardanoWasm.Transaction.from_bytes(rawTransaction);
} catch {
throw new BuildTransactionError('invalid raw transaction');
}
}
/** @inheritdoc */
validateTransaction(transaction: Transaction): void {
if (!transaction.transaction) {
return;
}
}
/** @inheritdoc */
validateValue(value: BigNumber): void {
if (value.isLessThan(0)) {
throw new BuildTransactionError('Value cannot be less than zero');
}
}
// endregion
/** @inheritDoc */
addSignature(publicKey: BasePublicKey, signature: Buffer): void {
this._signatures.push({ publicKey, signature });
}
private getAllSignatures(): Signature[] {
return this._initSignatures.concat(this._signatures);
}
}
Выполнить команду
Для локальной разработки. Не используйте в интернете!