import { AccountTransaction, AccountTransactionHeader, AccountTransactionInput, AccountTransactionType, ConfigureBakerHandler, ConfigureBakerPayload, ConfigureDelegationHandler, ConfigureDelegationPayload, DeployModuleHandler, DeployModulePayload, InitContractHandler, InitContractInput, InitContractPayload, MakeOptional, MakeRequired, RegisterDataHandler, RegisterDataPayload, SimpleTransferHandler, SimpleTransferPayload, SimpleTransferWithMemoHandler, SimpleTransferWithMemoPayload, TokenUpdateHandler, TokenUpdatePayload, UpdateContractHandler, UpdateContractInput, UpdateContractPayload, UpdateCredentialsHandler, UpdateCredentialsInput, UpdateCredentialsPayload, } from '../../index.js'; import * as JSONBig from '../../json-bigint.js'; import { AccountAddress, DataBlob, Energy, TransactionExpiry } from '../../types/index.js'; import { assertIn, isDefined } from '../../util.js'; import { AccountTransactionV0, AccountTransactionV1, Payload } from '../index.js'; import { Header, HeaderJSON, headerFromJSON, headerToJSON } from './shared.js'; import { type Signable, SignableJSON, SignableV0, SignableV1, isSignable, signableToJSON } from './signable.js'; type Transaction
= { /** * The transaction input header. */ readonly header: Header; /** * The transaction payload, defining the transaction type and type specific data. */ readonly payload: P; }; export type Type
= Transaction
;
export type BuilderJSON = {
header: HeaderJSON;
payload: Payload.JSON;
};
export type JSON = BuilderJSON | SignableJSON;
// --- Transaction construction ---
/**
* Base metadata input with optional expiry field.
*/
export type Metadata = MakeOptional = BuilderAPI & {
/**
* The transaction input header of the initial transaction stage, i.e. without metadata.
*/
readonly header: Pick = {
/**
* Adds metadata (sender, nonce, expiry) to the transaction, making it configured and ready to be signed.
*
* @template T - the transaction builder type
* @param metadata - transaction metadata including sender, nonce, and optionally expiry
*
* @returns a signable transaction with metadata attached
* @throws if transaction metadata already exists.
*/
addMetadata ;
/**
* Attempts to convert a builder to a _configured_ builder. This is useful in case type information is lost
* during (de)serialization.
*
* @example
* const tx = Transaction.transfer(...).addMetadata(...);
* const json = Transaction.fromJSON(Transaction.toJSON(t));
* const rebuilt = builder(json).configured();
*
* @template T - the transaction builder type
* @returns a _configured transaction builder if the transaction is properly configured to be buildable. Otherwise
* returns `undefined`.
*/
configured | undefined;
};
type Configured = Transaction > = Omit<
T,
keyof ConfiguredAPI | 'header'
> & {
/**
* The transaction input header of the pre-signed transaction stage, i.e. with metadata.
*/
readonly header: MakeRequired (transaction: Transaction ): transaction is Configured {
const {
header: { nonce, expiry, sender, executionEnergyAmount },
} = transaction as Configured ;
return isDefined(nonce) && isDefined(expiry) && isDefined(sender) && isDefined(executionEnergyAmount);
}
type MultiSigAPI = {
/**
* Configures the transaction for multi-signature by specifying the number of signatures required.
*
* @template T - the transaction builder type
* @param numSignaturesSender - the number of signatures required from the `sender` to authorize this transaction
*
* @returns a multi-sig transaction with the signature count configured
* @throws if number of sender signatures have already been added.
*/
addMultiSig ;
/**
* Attempts to convert a builder to a multi-sig configured builder. This is useful in case type information is lost
* during (de)serialization.
*
* @example
* const tx = Transaction.transfer(...).addMultiSig(4);
* const json = Transaction.fromJSON(Transaction.toJSON(t));
* const rebuilt = builder(json).multiSig();
*
* @template T - the transaction builder type
* @returns a multi-sig transaction builder if the transaction is properly configured for multi-sig. Otherwise
* returns `undefined`.
*/
multiSig | undefined;
};
type MultiSig = Transaction > = Omit<
T,
keyof MultiSigAPI | 'header'
> & {
/**
* The transaction input header of the multi-sig transaction stage, i.e. with the number of signatures
* defined.
*/
readonly header: MakeRequired (transaction: Transaction ): transaction is MultiSig {
const {
header: { numSignatures },
} = transaction as MultiSig ;
return isDefined(numSignatures) && numSignatures > 1n;
}
type SponsorableAPI = {
/**
* Configures the transaction for sponsorring by specifying the sponsor account.
* NOTE: this can be used from protocol version 10.
*
* @template T - the transaction builder type
* @param account - the sponsor account to use for sponsorring the transaction.
* @param [numSignaturesSponsor] - the number of signatures required to authorize this transaction. Defaults to `1` for if not specified.
*
* @returns a sponsorable transaction
* @throws if sponsor details already exits.
*/
addSponsor ;
/**
* Attempts to convert a builder to a multi-sig configured builder. This is useful in case type information is lost
* during (de)serialization.
*
* @example
* const tx = Transaction.transfer(...).addMultiSig(4);
* const json = Transaction.fromJSON(Transaction.toJSON(t));
* const rebuilt = builder(json).multiSig();
*
* @template T - the transaction builder type
* @returns a multi-sig transaction builder if the transaction is properly configured for multi-sig. Otherwise
* returns `undefined`.
*/
sponsorable | undefined;
};
type Sponsorable = Transaction > = Omit<
T,
keyof SponsorableAPI | keyof MultiSigAPI | 'header'
> & {
/**
* The transaction input header of the sponsorable transaction stage, i.e. with the sponsor details and
* the number of signatures defined.
*/
readonly header: MakeRequired = Readonly &
MultiSigAPI &
SponsorableAPI & {
/**
* Build the transaction to it's pre-finalized stage.
*/
build(this: Sponsorable & Configured ): SignableV1 ;
build(this: Configured ): Signable ;
/**
* Serializes the transaction to JSON format.
*
* @returns the JSON representation of the transaction
*/
toJSON(): BuilderJSON;
};
/**
* Type predicate checking if the transaction is a _signable_ transaction.
*
* @template P extends Payload.Type
* @param transaction - the transaction to check
* @returns whether the transaction is a _signable transaction
*/
export function isSponsorable (transaction: Transaction ): transaction is Sponsorable {
return 'sponsor' in transaction.header && isDefined(transaction.header.sponsor);
}
/**
* Describes an account transaction in its unprocessed form, i.e. defining the input required
* to create a transaction which can be signed
*/
export class Builder implements BuilderAPI {
constructor(
public readonly header: Header,
public readonly payload: P
) {}
public addMetadata {
if ([this.header.sender, this.header.nonce, this.header.expiry].every(isDefined))
throw new Error('Number of transaction metadata has already been specified.');
this.header.sender = sender;
this.header.nonce = nonce;
this.header.expiry = expiry;
return this as Configured ;
}
public configured | undefined {
if (isConfigured(this)) {
return this as Configured ;
}
}
public addMultiSig {
if (this.header.numSignatures !== undefined)
throw new Error('Number of transaction sender signatures has already been specified.');
this.header.numSignatures = BigInt(numSignaturesSender);
return this as MultiSig ;
}
public multiSig | undefined {
if (isMultiSig(this)) {
return this as MultiSig ;
}
}
public addSponsor {
if (this.header.sponsor !== undefined)
throw new Error('Number of transaction sponsor details have already been specified.');
this.header.numSignatures = this.header.numSignatures ?? 1n;
this.header.sponsor = { account, numSignatures: BigInt(numSignaturesSponsor) };
return this as Sponsorable ;
}
public sponsorable | undefined {
if (isSponsorable(this)) {
return this as Sponsorable ;
}
}
/**
* Build the transaction to it's pre-finalized stage.
*/
public build(this: Sponsorable & Configured ): SignableV1 ;
public build(this: Configured ): SignableV0 ;
public build(this: Configured ): Signable {
const {
header: { numSignatures = 1n },
payload,
} = this;
const header = { ...this.header, numSignatures };
if (isSponsorable(this)) {
return { version: 1, header, payload, signatures: { sender: {} } };
}
return { version: 0, header, payload, signature: {} };
}
/**
* Serializes the transaction to JSON format.
*
* @returns the JSON representation of the transaction
*/
public toJSON(): BuilderJSON {
return toJSON(this);
}
}
/**
* Dynamic `Transaction` creation based on the given transaction `type`.
*
* NOTE: this does _not_ check the payload structure, and thus assumes that the `type` and `payload`
* given actually match. If the transaction type is known, use the specialized creation functions
* per transaction type instead.
*
* @param type - transaction type
* @param payload - a transaction payload matching the transaction type.
*
* @returns The corresponding transaction
*
* @throws if transaction type is not currently supported.
* @throws if transaction cannot be created due to mismatch between `type` and `payload`.
*/
export function create(type: AccountTransactionType, payload: AccountTransactionInput): Initial {
switch (type) {
case AccountTransactionType.Transfer:
return transfer(payload as SimpleTransferPayload);
case AccountTransactionType.TransferWithMemo:
return transfer(payload as SimpleTransferWithMemoPayload);
case AccountTransactionType.DeployModule:
return deployModule(payload as DeployModulePayload);
case AccountTransactionType.InitContract:
const { maxContractExecutionEnergy: initEnergy, ...initPayload } = payload as InitContractInput;
return initContract(initPayload, initEnergy);
case AccountTransactionType.Update:
const { maxContractExecutionEnergy: updateEnergy, ...updatePayload } = payload as UpdateContractInput;
return updateContract(updatePayload, updateEnergy);
case AccountTransactionType.UpdateCredentials:
const { currentNumberOfCredentials, ...credPayload } = payload as UpdateCredentialsInput;
return updateCredentials(credPayload, currentNumberOfCredentials);
case AccountTransactionType.RegisterData:
return registerData(payload as RegisterDataPayload);
case AccountTransactionType.ConfigureDelegation:
return configureDelegation(payload as ConfigureDelegationPayload);
case AccountTransactionType.ConfigureBaker:
return configureValidator(payload as ConfigureBakerPayload);
case AccountTransactionType.TokenUpdate:
return tokenUpdate(payload as TokenUpdatePayload);
default:
throw new Error('The provided type is not supported: ' + type);
}
}
/**
* Crates a {@linkcode Transaction} builder object from the legacy `AccountTransaction` format.
*
* @param transaction - The {@linkcode AccountTransaction} to convert.
* @returns a corresonding transaction builder object.
*/
export function fromLegacyAccountTransaction({ type, header, payload }: AccountTransaction): Configured {
return create(type, payload).addMetadata(header);
}
/**
* Converts a {@linkcode Transaction} to the legacy format.
*
* @param transaction - the transaction details to convert
* @returns the legacy {@linkcode AccountTransaction} format
*/
export function toLegacyAccountTransaction(transaction: Transaction): AccountTransaction {
const {
header: { numSignatures, executionEnergyAmount, ...header },
payload: { type, ...payload },
} = transaction;
switch (type) {
case AccountTransactionType.InitContract:
case AccountTransactionType.Update:
return {
header,
type,
payload: { ...payload, maxContractExecutionEnergy: executionEnergyAmount },
} as AccountTransaction<
AccountTransactionType.Update | AccountTransactionType.InitContract,
InitContractInput | UpdateContractInput
>;
default:
return { header, type, payload } as AccountTransaction<
Exclude (payload: P | Omit ): payload is P =>
(payload as P).type !== undefined;
const isWithMemo = (
payload: SimpleTransferPayload | SimpleTransferWithMemoPayload
): payload is SimpleTransferWithMemoPayload => (payload as SimpleTransferWithMemoPayload).memo !== undefined;
/**
* Creates a transfer transaction with memo
* @param payload the transfer payload containing recipient and amount and memo
* @returns a transfer with memo transaction
*/
export function transfer(
payload: SimpleTransferWithMemoPayload | Payload.TransferWithMemo
): Initial