PHP WebShell
Текущая директория: /opt/BitGoJS/modules/sdk-coin-xtz/src/lib
Просмотр файла: transactionBuilder.ts
import { BaseKey, BuildTransactionError, SigningError, BaseTransactionBuilder, TransactionType } from '@bitgo/sdk-core';
import { BaseCoin as CoinConfig } from '@bitgo/statics';
import BigNumber from 'bignumber.js';
import { Address } from './address';
import { Fee, IndexedData, IndexedSignature, Key, Operation, OriginationOp, RevealOp, TransactionOp } from './iface';
import { KeyPair } from './keyPair';
import {
forwarderOriginationOperation,
genericMultisigOriginationOperation,
multisigTransactionOperation,
revealOperation,
singlesigTransactionOperation,
} from './multisigUtils';
import { Transaction } from './transaction';
import { TransferBuilder } from './transferBuilder';
import {
DEFAULT_GAS_LIMIT,
DEFAULT_STORAGE_LIMIT,
isValidAddress,
isValidBlockHash,
isValidOriginatedAddress,
isValidPublicKey,
sign,
} from './utils';
const DEFAULT_M = 3;
interface DataToSignOverride extends IndexedData {
dataToSign: string;
}
interface IndexedKeyPair extends IndexedData {
key: KeyPair;
}
/**
* Tezos transaction builder.
*/
export class TransactionBuilder extends BaseTransactionBuilder {
private _serializedTransaction: string;
private _transaction: Transaction;
private _type: TransactionType;
private _blockHeader: string;
private _counter: BigNumber;
private _fee: Fee;
private _sourceAddress: string;
private _sourceKeyPair?: KeyPair;
// Public key revelation transaction parameters
private _publicKeyToReveal: string;
// Wallet initialization transaction parameters
private _initialBalance: string;
private _initialDelegate: string;
private _walletOwnerPublicKeys: string[];
// Send transaction parameters
private _multisigSignerKeyPairs: IndexedKeyPair[];
private _dataToSignOverride: DataToSignOverride[];
private _transfers: TransferBuilder[];
// Address initialization parameters
private _forwarderDestination: string;
/**
* Public constructor.
*
* @param {CoinConfig} _coinConfig - coin configuration
*/
constructor(_coinConfig: Readonly<CoinConfig>) {
super(_coinConfig);
this._type = TransactionType.Send;
this._counter = new BigNumber(0);
this._transfers = [];
this._walletOwnerPublicKeys = [];
this._multisigSignerKeyPairs = [];
this._dataToSignOverride = [];
this.transaction = new Transaction(_coinConfig);
}
// region Base Builder
/** @inheritdoc */
protected fromImplementation(rawTransaction: string): Transaction {
// Decoding the transaction is an async operation, so save it and leave the decoding for the
// build step
this._serializedTransaction = rawTransaction;
return new Transaction(this._coinConfig);
}
/** @inheritdoc */
protected signImplementation(key: Key): Transaction {
const signer = new KeyPair({ prv: key.key });
// Currently public key revelation is the only type of account update tx supported in Tezos
if (this._type === TransactionType.AccountUpdate && !this._publicKeyToReveal) {
throw new SigningError('Cannot sign a public key revelation transaction without public key');
}
if (this._type === TransactionType.WalletInitialization && this._walletOwnerPublicKeys.length === 0) {
throw new SigningError('Cannot sign an wallet initialization transaction without owners');
}
if (
this._type === TransactionType.Send &&
this._transfers.length === 0 &&
this._serializedTransaction === undefined
) {
throw new SigningError('Cannot sign an empty send transaction');
}
if (this._type === TransactionType.Send && (!this._sourceAddress || this._sourceAddress !== signer.getAddress())) {
// If the signer is not the source and it is a send transaction, add it to the list of
// multisig wallet signers
// TODO: support a combination of keys with and without custom index
if (key.index && key.index >= DEFAULT_M) {
throw new BuildTransactionError(
'Custom index cannot be greater than the wallet total number of signers (owners)'
);
}
// Make sure either all keys passed have a custom index or none of them have
const shouldHaveCustomIndex = key.hasOwnProperty('index');
for (let i = 0; i < this._multisigSignerKeyPairs.length; i++) {
if (shouldHaveCustomIndex !== (this._multisigSignerKeyPairs[i].index !== undefined)) {
throw new BuildTransactionError('Custom index has to be set for all multisig contract signing keys or none');
}
}
const multisigSignerKey = shouldHaveCustomIndex ? { key: signer, index: key.index } : { key: signer };
this._multisigSignerKeyPairs.push(multisigSignerKey);
} else {
if (this._sourceKeyPair) {
throw new SigningError('Cannot sign multiple times a non send-type transaction');
}
this._sourceKeyPair = signer;
}
// Signing the transaction is an async operation, so save the source and leave the actual
// signing for the build step
return this.transaction;
}
/** @inheritdoc */
protected async buildImplementation(): Promise<Transaction> {
// If the from() method was called, use the serialized transaction as a base
if (this._serializedTransaction) {
await this.transaction.initFromSerializedTransaction(this._serializedTransaction);
for (let i = 0; i < this._dataToSignOverride.length; i++) {
const signatures = await this.getSignatures(this._dataToSignOverride[i].dataToSign);
await this.transaction.addTransferSignature(signatures, this._dataToSignOverride[i].index || i);
}
// TODO: make changes to the transaction if any extra parameter has been set then sign it
} else {
let contents: Operation[] = [];
switch (this._type) {
case TransactionType.AccountUpdate:
if (this._publicKeyToReveal) {
contents.push(this.buildPublicKeyRevelationOperation());
}
break;
case TransactionType.WalletInitialization:
if (this._publicKeyToReveal) {
contents.push(this.buildPublicKeyRevelationOperation());
}
contents.push(this.buildWalletInitializationOperations());
break;
case TransactionType.Send:
if (this._publicKeyToReveal) {
contents.push(this.buildPublicKeyRevelationOperation());
}
contents = contents.concat(await this.buildSendTransactionContent());
break;
case TransactionType.AddressInitialization:
if (this._publicKeyToReveal) {
contents.push(this.buildPublicKeyRevelationOperation());
}
contents = contents.concat(this.buildForwarderDeploymentContent());
break;
case TransactionType.SingleSigSend:
// No support for revelation txns as primary use case is to send from fee address
contents = contents.concat(await this.buildSendTransactionContent());
break;
default:
throw new BuildTransactionError('Unsupported transaction type');
}
if (contents.length === 0) {
throw new BuildTransactionError('Empty transaction');
}
const parsedTransaction = {
branch: this._blockHeader,
contents,
};
this.transaction = new Transaction(this._coinConfig);
// Build and sign a new transaction based on the latest changes
await this.transaction.initFromParsedTransaction(parsedTransaction);
}
if (this._sourceKeyPair && this._sourceKeyPair.getKeys().prv) {
// TODO: check if there are more signers than needed for a singlesig or multisig transaction
await this.transaction.sign(this._sourceKeyPair);
}
return this.transaction;
}
// endregion
// region Common builder methods
/**
* Set the transaction branch id.
*
* @param {string} blockId A block hash to use as branch reference
*/
branch(blockId: string): void {
if (!isValidBlockHash(blockId)) {
throw new BuildTransactionError('Invalid block hash ' + blockId);
}
this._blockHeader = blockId;
}
/**
* The type of transaction being built.
*
* @param {TransactionType} type - type of the transaction
*/
type(type: TransactionType): void {
if (type === TransactionType.Send && this._walletOwnerPublicKeys.length > 0) {
throw new BuildTransactionError('Transaction cannot be labeled as Send when owners have already been set');
}
if (type !== TransactionType.Send && this._transfers.length > 0) {
throw new BuildTransactionError('Transaction contains transfers and can only be labeled as Send');
}
this._type = type;
}
/**
* Set the transaction fees. Low fees may get a transaction rejected or never picked up by bakers.
*
* @param {Fee} fee Baker fees. May also include the maximum gas and storage fees to pay
*/
fee(fee: Fee): void {
this.validateValue(new BigNumber(fee.fee));
if (fee.gasLimit) {
this.validateValue(new BigNumber(fee.gasLimit));
}
if (fee.storageLimit) {
this.validateValue(new BigNumber(fee.storageLimit));
}
this._fee = fee;
}
/**
* Set the transaction initiator. This account will pay for the transaction fees, but it will not
* be added as an owner of a wallet in a init transaction, unless manually set as one of the
* owners.
*
* @param {string} source A Tezos address
*/
source(source: string): void {
this.validateAddress({ address: source });
this._sourceAddress = source;
}
/**
* Set an amount of mutez to transfer in this transaction this transaction. This is different than
* the amount to transfer from a multisig wallet.
*
* @param {string} amount Amount in mutez (1/1000000 Tezies)
*/
initialBalance(amount: string): void {
if (this._type !== TransactionType.WalletInitialization) {
throw new BuildTransactionError('Initial balance can only be set for wallet initialization transactions');
}
this.validateValue(new BigNumber(amount));
this._initialBalance = amount;
}
/**
* Set the transaction counter to prevent submitting repeated transactions.
*
* @param {string} counter The counter to use
*/
counter(counter: string): void {
this._counter = new BigNumber(counter);
}
/**
* Set the destination address of a forwarder contract
* Used in forwarder contract deployment as destination address
*
* @param {string} contractAddress - contract address to use
*/
forwarderDestination(contractAddress: string): void {
if (this._type !== TransactionType.AddressInitialization) {
throw new BuildTransactionError('Forwarder destination can only be set for address initialization transactions');
}
if (!isValidOriginatedAddress(contractAddress)) {
throw new BuildTransactionError('Forwarder destination can only be an originated address');
}
this._forwarderDestination = contractAddress;
}
// endregion
// region PublicKeyRevelation builder methods
/**
* The public key to reveal.
*
* @param {string} publicKey A Tezos public key
*/
publicKeyToReveal(publicKey: string): void {
if (this._publicKeyToReveal) {
throw new BuildTransactionError('Public key to reveal already set: ' + this._publicKeyToReveal);
}
const keyPair = new KeyPair({ pub: publicKey });
if (keyPair.getAddress() !== this._sourceAddress) {
throw new BuildTransactionError('Public key does not match the source address: ' + this._sourceAddress);
}
this._publicKeyToReveal = keyPair.getKeys().pub;
}
/**
* Build a reveal operation for the source account with default fees.
*
* @returns {RevealOp} A Tezos reveal operation
*/
private buildPublicKeyRevelationOperation(): RevealOp {
const operation = revealOperation(this._counter.toString(), this._sourceAddress, this._publicKeyToReveal);
this._counter = this._counter.plus(1);
return operation;
}
// endregion
// region WalletInitialization builder methods
/**
* Set one of the owners of the multisig wallet.
*
* @param {string} publicKey A Tezos public key
*/
owner(publicKey: string): void {
if (this._type !== TransactionType.WalletInitialization) {
throw new BuildTransactionError('Multisig wallet owner can only be set for initialization transactions');
}
if (this._walletOwnerPublicKeys.length >= DEFAULT_M) {
throw new BuildTransactionError('A maximum of ' + DEFAULT_M + ' owners can be set for a multisig wallet');
}
if (!isValidPublicKey(publicKey)) {
throw new BuildTransactionError('Invalid public key: ' + publicKey);
}
if (this._walletOwnerPublicKeys.includes(publicKey)) {
throw new BuildTransactionError('Repeated owner public key: ' + publicKey);
}
this._walletOwnerPublicKeys.push(publicKey);
}
/**
* Set an initial delegate to initialize this wallet to. This is different than the delegation to
* set while doing a separate delegation transaction.
*
* @param {string} delegate The address to delegate the funds to
*/
initialDelegate(delegate: string): void {
if (this._type !== TransactionType.WalletInitialization) {
throw new BuildTransactionError('Initial delegation can only be set for wallet initialization transactions');
}
this.validateAddress({ address: delegate });
this._initialDelegate = delegate;
}
/**
* Build an origination operation for a generic multisig contract.
*
* @returns {Operation} A Tezos origination operation
*/
private buildWalletInitializationOperations(): OriginationOp {
const originationOp = genericMultisigOriginationOperation(
this._counter.toString(),
this._sourceAddress,
this._fee.fee,
this._fee.gasLimit || DEFAULT_GAS_LIMIT.ORIGINATION.toString(),
this._fee.storageLimit || DEFAULT_STORAGE_LIMIT.ORIGINATION.toString(),
this._initialBalance || '0',
this._walletOwnerPublicKeys,
this._initialDelegate
);
this._counter = this._counter.plus(1);
return originationOp;
}
// endregion
// region Send builder methods
/**
* Initialize a new TransferBuilder to for a singlesig or multisig transaction.
*
* @param {string} amount Amount in mutez to be transferred
* @returns {TransferBuilder} A transfer builder
*/
transfer(amount: string): TransferBuilder {
if (this._type !== TransactionType.Send && this._type !== TransactionType.SingleSigSend) {
throw new BuildTransactionError('Transfers can only be set for send transactions');
}
let transferBuilder = new TransferBuilder();
// If source was set, use it as default for
if (this._sourceAddress) {
transferBuilder = transferBuilder.from(this._sourceAddress);
}
if (this._fee) {
transferBuilder = transferBuilder.fee(this._fee.fee);
transferBuilder = this._fee.gasLimit ? transferBuilder.gasLimit(this._fee.gasLimit) : transferBuilder;
transferBuilder = this._fee.storageLimit ? transferBuilder.storageLimit(this._fee.storageLimit) : transferBuilder;
}
this._transfers.push(transferBuilder);
return transferBuilder.amount(amount);
}
/**
* Calculate the signatures for the multisig transaction.
*
* @param {string} packedData The string in hexadecimal to sign
* @returns {Promise<string[]>} List of signatures for packedData
*/
private async getSignatures(packedData: string): Promise<IndexedSignature[]> {
const signatures: IndexedSignature[] = [];
// Generate the multisig contract signatures
for (let i = 0; i < this._multisigSignerKeyPairs.length; i++) {
const signature = await sign(this._multisigSignerKeyPairs[i].key, packedData, new Uint8Array(0));
const index = this._multisigSignerKeyPairs[i].index;
signatures.push(index ? { signature: signature.sig, index } : { signature: signature.sig });
}
return signatures;
}
/**
* Override the data to sign for a specific transfer. Used for offline signing to pass the
* respective dataToSign for transfer at a particular index.
*
* @param {DataToSignOverride} data - data to override
*/
overrideDataToSign(data: DataToSignOverride): void {
if (!data.index) {
data.index = this._dataToSignOverride.length;
}
this._dataToSignOverride.push(data);
}
/**
* Build a transaction operation for a generic multisig contract.
*
* @returns {Promise<TransactionOp[]>} A Tezos transaction operation
*/
private async buildSendTransactionContent(): Promise<TransactionOp[]> {
const contents: TransactionOp[] = [];
for (let i = 0; i < this._transfers.length; i++) {
const transfer = this._transfers[i].build();
let transactionOp;
if (isValidOriginatedAddress(transfer.from)) {
// Offline transactions may not have the data to sign
const signatures = transfer.dataToSign ? await this.getSignatures(transfer.dataToSign) : [];
transactionOp = multisigTransactionOperation(
this._counter.toString(),
this._sourceAddress,
transfer.amount,
transfer.from,
transfer.counter || '0',
transfer.to,
signatures,
transfer.fee.fee,
transfer.fee.gasLimit,
transfer.fee.storageLimit
);
} else {
transactionOp = singlesigTransactionOperation(
this._counter.toString(),
this._sourceAddress,
transfer.amount,
transfer.to,
transfer.fee.fee,
transfer.fee.gasLimit,
transfer.fee.storageLimit
);
}
contents.push(transactionOp);
this._counter = this._counter.plus(1);
}
return contents;
}
// endregion
// region ForwarderAddressDeployment
/**
* Build a transaction operation for a forwarder contract
*
* @returns {OriginationOp} a Tezos transaction operation
*/
private buildForwarderDeploymentContent(): OriginationOp {
const operation = forwarderOriginationOperation(
this._forwarderDestination,
this._counter.toString(),
this._sourceAddress,
this._fee.fee,
this._fee.gasLimit || DEFAULT_GAS_LIMIT.ORIGINATION.toString(),
this._fee.storageLimit || DEFAULT_STORAGE_LIMIT.ORIGINATION.toString(),
this._initialBalance || '0'
);
this._counter = this._counter.plus(1);
return operation;
}
// endregion
// region Validators
/** @inheritdoc */
validateValue(value: BigNumber): void {
if (value.isLessThan(0)) {
throw new BuildTransactionError('Value cannot be below less than zero');
}
// TODO: validate the amount is not bigger than the max amount in Tezos
}
/** @inheritdoc */
validateAddress(address: Address): void {
if (!isValidAddress(address.address)) {
throw new BuildTransactionError('Invalid address ' + address.address);
}
}
/** @inheritdoc */
validateKey(key: BaseKey): void {
const keyPair = new KeyPair({ prv: key.key });
if (!keyPair.getKeys().prv) {
throw new BuildTransactionError('Invalid key');
}
}
/** @inheritdoc */
validateRawTransaction(rawTransaction: any): void {
// TODO: validate the transaction is either a JSON or a hex
}
/** @inheritdoc */
validateTransaction(transaction: Transaction): void {
// TODO: validate all required fields are present in the builder before buildImplementation
switch (this._type) {
case TransactionType.AccountUpdate:
break;
case TransactionType.WalletInitialization:
break;
case TransactionType.Send:
break;
case TransactionType.AddressInitialization:
break;
case TransactionType.SingleSigSend:
break;
default:
throw new BuildTransactionError('Transaction type not supported');
}
}
// endregion
/** @inheritdoc */
displayName(): string {
return this._coinConfig.fullName;
}
/** @inheritdoc */
protected get transaction(): Transaction {
return this._transaction;
}
/** @inheritdoc */
protected set transaction(transaction: Transaction) {
this._transaction = transaction;
}
}
Выполнить команду
Для локальной разработки. Не используйте в интернете!