Skip to content

Commit

Permalink
Merge pull request #5323 from BitGo/coin-2258-apt-tx-builder-3
Browse files Browse the repository at this point in the history
feat(sdk-coin-apt): transaction builder
  • Loading branch information
bhavidhingra committed Jan 2, 2025
2 parents 48d7edf + 87df169 commit 99e7b80
Show file tree
Hide file tree
Showing 10 changed files with 370 additions and 49 deletions.
1 change: 1 addition & 0 deletions modules/sdk-coin-apt/src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export const APT_ADDRESS_LENGTH = 64;
export const APT_TRANSACTION_ID_LENGTH = 64;
export const APT_BLOCK_ID_LENGTH = 64;
export const APT_SIGNATURE_LENGTH = 128;
export const UNAVAILABLE_TEXT = 'UNAVAILABLE';
22 changes: 6 additions & 16 deletions modules/sdk-coin-apt/src/lib/iface.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,17 @@
import {
TransactionExplanation as BaseTransactionExplanation,
TransactionType as BitGoTransactionType,
} from '@bitgo/sdk-core';
import { ITransactionExplanation, TransactionFee } from '@bitgo/sdk-core';

export enum AptTransactionType {
Transfer = 'Transfer',
TokenTransfer = 'TokenTransfer',
}

export interface TransactionExplanation extends BaseTransactionExplanation {
type: BitGoTransactionType;
}
export type TransactionExplanation = ITransactionExplanation<TransactionFee>;

/**
* The transaction data returned from the toJson() function of a transaction
*/
export interface TxData {
id: string;
sender: string;
sequenceNumber: BigInt;
maxGasAmount: BigInt;
gasUnitPrice: BigInt;
expirationTime: BigInt;
sequenceNumber: number;
maxGasAmount: number;
gasUnitPrice: number;
expirationTime: number;
payload: AptPayload;
chainId: number;
}
Expand Down
3 changes: 2 additions & 1 deletion modules/sdk-coin-apt/src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import * as Utils from './utils';
import * as Interface from './iface';

export { KeyPair } from './keyPair';
export { Transaction } from './transaction';
export { Transaction } from './transaction/transaction';
export { TransferTransaction } from './transaction/transferTransaction';
export { TransactionBuilder } from './transactionBuilder';
export { TransferBuilder } from './transferBuilder';
export { TransactionBuilderFactory } from './transactionBuilderFactory';
Expand Down
16 changes: 0 additions & 16 deletions modules/sdk-coin-apt/src/lib/transaction.ts

This file was deleted.

181 changes: 181 additions & 0 deletions modules/sdk-coin-apt/src/lib/transaction/transaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import {
BaseKey,
BaseTransaction,
InvalidTransactionError,
PublicKey,
Signature,
TransactionRecipient,
TransactionType,
} from '@bitgo/sdk-core';
import { TransactionExplanation, TxData } from '../iface';
import { BaseCoin as CoinConfig } from '@bitgo/statics';
import {
AccountAddress,
AccountAuthenticatorEd25519,
Ed25519PublicKey,
Ed25519Signature,
generateUserTransactionHash,
RawTransaction,
SignedTransaction,
SimpleTransaction,
TransactionAuthenticatorEd25519,
} from '@aptos-labs/ts-sdk';
import { UNAVAILABLE_TEXT } from '../constants';
import utils from '../utils';

export abstract class Transaction extends BaseTransaction {
protected _rawTransaction: RawTransaction;
protected _signature: Signature;

static DEFAULT_PUBLIC_KEY = Buffer.alloc(32);
static DEFAULT_SIGNATURE = Buffer.alloc(64);

constructor(coinConfig: Readonly<CoinConfig>) {
super(coinConfig);
}

/** @inheritDoc **/
public get id(): string {
return this._id ?? UNAVAILABLE_TEXT;
}

set transactionType(transactionType: TransactionType) {
this._type = transactionType;
}

public get signablePayload(): Buffer {
const rawTxnHex = this._rawTransaction.bcsToHex().toString();
return Buffer.from(rawTxnHex, 'hex');
}

public get sender(): string {
return this._rawTransaction.sender.toString();
}

set sender(senderAddress: string) {
// Cannot assign to 'sender' because it is a read-only property in RawTransaction.
const { sequence_number, payload, max_gas_amount, gas_unit_price, expiration_timestamp_secs, chain_id } =
this._rawTransaction;
this._rawTransaction = new RawTransaction(
AccountAddress.fromString(senderAddress),
sequence_number,
payload,
max_gas_amount,
gas_unit_price,
expiration_timestamp_secs,
chain_id
);
}

public get recipient(): TransactionRecipient {
return utils.getRecipientFromTransactionPayload(this._rawTransaction.payload);
}

canSign(_key: BaseKey): boolean {
return false;
}

toBroadcastFormat(): string {
if (!this._rawTransaction) {
throw new InvalidTransactionError('Empty transaction');
}
return this.serialize();
}

serialize(): string {
let publicKeyBuffer = Transaction.DEFAULT_PUBLIC_KEY;
let signatureBuffer = Transaction.DEFAULT_SIGNATURE;
if (this._signature && this._signature.publicKey && this._signature.signature) {
publicKeyBuffer = Buffer.from(this._signature.publicKey.pub, 'hex');
signatureBuffer = this._signature.signature;
}
const publicKey = new Ed25519PublicKey(publicKeyBuffer);
const signature = new Ed25519Signature(signatureBuffer);
const txnAuthenticator = new TransactionAuthenticatorEd25519(publicKey, signature);
const signedTxn = new SignedTransaction(this._rawTransaction, txnAuthenticator);
return signedTxn.toString();
}

abstract toJson(): TxData;

addSignature(publicKey: PublicKey, signature: Buffer): void {
const publicKeyBuffer = Buffer.from(publicKey.pub, 'hex');
if (!Transaction.DEFAULT_PUBLIC_KEY.equals(publicKeyBuffer) && !Transaction.DEFAULT_SIGNATURE.equals(signature)) {
this._signatures.push(signature.toString('hex'));
this._signature = { publicKey, signature };
this.serialize();
}
}

async build(): Promise<void> {
this.loadInputsAndOutputs();
if (this._signature && this._signature.publicKey && this._signature.signature) {
const transaction = new SimpleTransaction(this._rawTransaction);
const publicKey = new Ed25519PublicKey(Buffer.from(this._signature.publicKey.pub, 'hex'));
const signature = new Ed25519Signature(this._signature.signature);
const senderAuthenticator = new AccountAuthenticatorEd25519(publicKey, signature);
this._id = generateUserTransactionHash({ transaction, senderAuthenticator });
}
}

loadInputsAndOutputs(): void {
const txRecipient = this.recipient;
this._inputs = [
{
address: this.sender,
value: txRecipient.amount as string,
coin: this._coinConfig.name,
},
];
this._outputs = [
{
address: txRecipient.address,
value: txRecipient.amount as string,
coin: this._coinConfig.name,
},
];
}

fromRawTransaction(rawTransaction: string): void {
try {
const signedTxn = utils.deserializeSignedTransaction(rawTransaction);
this._rawTransaction = signedTxn.raw_txn;

this.loadInputsAndOutputs();

const authenticator = signedTxn.authenticator as TransactionAuthenticatorEd25519;
const publicKey = Buffer.from(authenticator.public_key.toUint8Array());
const signature = Buffer.from(authenticator.signature.toUint8Array());
this.addSignature({ pub: publicKey.toString() }, signature);
} catch (e) {
console.error('invalid raw transaction', e);
throw new Error('invalid raw transaction');
}
}

/** @inheritDoc */
explainTransaction(): TransactionExplanation {
const displayOrder = ['id', 'outputs', 'outputAmount', 'changeOutputs', 'changeAmount', 'fee', 'withdrawAmount'];

const outputs: TransactionRecipient[] = [this.recipient];
const outputAmount = outputs[0].amount;
return {
displayOrder,
id: this.id,
outputs,
outputAmount,
changeOutputs: [],
changeAmount: '0',
fee: { fee: 'UNKNOWN' },
};
}

static deserializeRawTransaction(rawTransaction: string): RawTransaction {
try {
return utils.deserializeRawTransaction(rawTransaction);
} catch (e) {
console.error('invalid raw transaction', e);
throw new Error('invalid raw transaction');
}
}
}
26 changes: 26 additions & 0 deletions modules/sdk-coin-apt/src/lib/transaction/transferTransaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Transaction } from './transaction';
import { TxData } from '../iface';
import { TransactionPayloadEntryFunction } from '@aptos-labs/ts-sdk';

export class TransferTransaction extends Transaction {
toJson(): TxData {
const rawTxn = this._rawTransaction;
const payload = this._rawTransaction.payload as TransactionPayloadEntryFunction;
const entryFunction = payload.entryFunction;
return {
id: this.id,
sender: this.sender,
sequenceNumber: rawTxn.sequence_number as number,
maxGasAmount: rawTxn.max_gas_amount as number,
gasUnitPrice: rawTxn.gas_unit_price as number,
expirationTime: rawTxn.expiration_timestamp_secs as number,
payload: {
function: entryFunction.function_name.identifier,
typeArguments: entryFunction.type_args.map((a) => a.toString()),
arguments: entryFunction.args.map((a) => a.toString()),
type: 'entry_function_payload',
},
chainId: rawTxn.chain_id.chainId,
};
}
}
45 changes: 34 additions & 11 deletions modules/sdk-coin-apt/src/lib/transactionBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,24 @@ import {
BaseKey,
BaseTransactionBuilder,
BuildTransactionError,
FeeOptions,
ParseTransactionError,
PublicKey as BasePublicKey,
Signature,
TransactionType,
} from '@bitgo/sdk-core';
import { Transaction } from './transaction';
import { Transaction } from './transaction/transaction';
import utils from './utils';
import BigNumber from 'bignumber.js';
import { BaseCoin as CoinConfig } from '@bitgo/statics';

export abstract class TransactionBuilder extends BaseTransactionBuilder {
protected _transaction: Transaction;
private _signatures: Signature[] = [];

constructor(coinConfig: Readonly<CoinConfig>) {
super(coinConfig);
}

// get and set region
/**
* The transaction type.
Expand All @@ -40,6 +45,7 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder {
/** @inheritDoc */
addSignature(publicKey: BasePublicKey, signature: Buffer): void {
this._signatures.push({ publicKey, signature });
this.transaction.addSignature(publicKey, signature);
}

/**
Expand All @@ -50,23 +56,32 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder {
* @returns {TransactionBuilder} This transaction builder
*/
sender(senderAddress: string): this {
throw new Error('Method not implemented.');
}

fee(feeOptions: FeeOptions): this {
throw new Error('Method not implemented.');
this.validateAddress({ address: senderAddress });
this._transaction.sender = senderAddress;
return this;
}

/** @inheritdoc */
protected fromImplementation(rawTransaction: string): Transaction {
throw new Error('Method not implemented.');
this.transaction.fromRawTransaction(rawTransaction);
this.transaction.transactionType = this.transactionType;
return this.transaction;
}

/** @inheritdoc */
protected async buildImplementation(): Promise<Transaction> {
throw new Error('Method not implemented.');
this.transaction.transactionType = this.transactionType;
await this.transaction.build();
return this.transaction;
}

/**
* Initialize the transaction builder fields using the decoded transaction data
*
* @param {Transaction} tx the transaction data
*/
abstract initBuilder(tx: Transaction): void;

// region Validators
/** @inheritdoc */
validateAddress(address: BaseAddress, addressFormat?: string): void {
Expand All @@ -82,12 +97,20 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder {

/** @inheritdoc */
validateRawTransaction(rawTransaction: string): void {
throw new Error('Method not implemented.');
if (!rawTransaction) {
throw new ParseTransactionError('Invalid raw transaction: Undefined');
}
if (!utils.isValidRawTransaction(rawTransaction)) {
throw new ParseTransactionError('Invalid raw transaction');
}
}

/** @inheritdoc */
validateTransaction(transaction?: Transaction): void {
throw new Error('Method not implemented.');
if (!transaction) {
throw new Error('transaction not defined');
}
this.validateRawTransaction(transaction.toBroadcastFormat());
}

/** @inheritdoc */
Expand Down
Loading

0 comments on commit 99e7b80

Please sign in to comment.