Skip to content

Commit

Permalink
feat(sdk-coin-apt): fungible asset transfer transaction
Browse files Browse the repository at this point in the history
TICKET: COIN-2894
  • Loading branch information
baltiyal committed Jan 29, 2025
1 parent 96f6801 commit d8b7424
Show file tree
Hide file tree
Showing 17 changed files with 527 additions and 188 deletions.
6 changes: 5 additions & 1 deletion modules/sdk-coin-apt/src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,8 @@ export const DEFAULT_GAS_UNIT_PRICE = 100;
export const SECONDS_PER_WEEK = 7 * 24 * 60 * 60; // Days * Hours * Minutes * Seconds

export const APTOS_ACCOUNT_MODULE = 'aptos_account';
export const FUNGIBLE_ASSET_MODULE = 'fungible_asset';
export const FUNGIBLE_ASSET_MODULE = 'primary_fungible_store';

export const FUNGIBLE_ASSET_FUNCTION = '0x1::primary_fungible_store::transfer';

export const FUNGIBLE_ASSET = '0x1::fungible_asset::Metadata';
3 changes: 2 additions & 1 deletion modules/sdk-coin-apt/src/lib/iface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export interface AptTransactionExplanation extends BaseTransactionExplanation {
/**
* The transaction data returned from the toJson() function of a transaction
*/
export interface TransferTxData {
export interface TxData {
id: string;
sender: string;
recipient: TransactionRecipient;
Expand All @@ -22,4 +22,5 @@ export interface TransferTxData {
gasUsed: number;
expirationTime: number;
feePayer: string;
assetId: string;
}
4 changes: 2 additions & 2 deletions modules/sdk-coin-apt/src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as Interface from './iface';
export { KeyPair } from './keyPair';
export { Transaction } from './transaction/transaction';
export { TransferTransaction } from './transaction/transferTransaction';
export { TransactionBuilder } from './transactionBuilder';
export { TransferBuilder } from './transferBuilder';
export { TransactionBuilder } from './transactionBuilder/transactionBuilder';
export { TransferBuilder } from './transactionBuilder/transferBuilder';
export { TransactionBuilderFactory } from './transactionBuilderFactory';
export { Interface, Utils };
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { Transaction } from './transaction';
import {
AccountAddress,
Aptos,
AptosConfig,
EntryFunctionABI,
Network,
parseTypeTag,
TransactionPayload,
TransactionPayloadEntryFunction,
TypeTagAddress,
TypeTagU64,
} from '@aptos-labs/ts-sdk';
import { InvalidTransactionError, TransactionRecipient, TransactionType } from '@bitgo/sdk-core';
import { BaseCoin as CoinConfig, NetworkType } from '@bitgo/statics';
import { FUNGIBLE_ASSET, FUNGIBLE_ASSET_FUNCTION } from '../constants';

export class FungibleAssetTransaction extends Transaction {
constructor(coinConfig: Readonly<CoinConfig>) {
super(coinConfig);
this._type = TransactionType.SendToken;
}

protected parseTransactionPayload(payload: TransactionPayload): void {
if (
!(payload instanceof TransactionPayloadEntryFunction) ||
payload.entryFunction.args.length !== 3 ||
payload.entryFunction.type_args.length === 0 ||
FUNGIBLE_ASSET !== payload.entryFunction.type_args[0].toString()
) {
throw new InvalidTransactionError('Invalid transaction payload');
}
const entryFunction = payload.entryFunction;
if (!this._recipient) {
this._recipient = {} as TransactionRecipient;
}
this._assetId = entryFunction.args[0].toString();
this._recipient.address = entryFunction.args[1].toString();
const amountBuffer = Buffer.from(entryFunction.args[2].bcsToBytes());
this._recipient.amount = amountBuffer.readBigUint64LE().toString();
}

protected async buildRawTransaction(): Promise<void> {
const network: Network = this._coinConfig.network.type === NetworkType.MAINNET ? Network.MAINNET : Network.TESTNET;
const aptos = new Aptos(new AptosConfig({ network }));
const senderAddress = AccountAddress.fromString(this._sender);
const recipientAddress = AccountAddress.fromString(this._recipient.address);
const fungibleTokenAddress = this._assetId;

const faTransferAbi: EntryFunctionABI = {
typeParameters: [{ constraints: [] }],
parameters: [parseTypeTag('0x1::object::Object'), new TypeTagAddress(), new TypeTagU64()],
};

const simpleTxn = await aptos.transaction.build.simple({
sender: senderAddress,
data: {
function: FUNGIBLE_ASSET_FUNCTION,
typeArguments: [FUNGIBLE_ASSET],
functionArguments: [fungibleTokenAddress, recipientAddress, this.recipient.amount],
abi: faTransferAbi,
},
options: {
maxGasAmount: this.maxGasAmount,
gasUnitPrice: this.gasUnitPrice,
expireTimestamp: this.expirationTime,
accountSequenceNumber: this.sequenceNumber,
},
});
this._rawTransaction = simpleTxn.rawTransaction;
}
}
71 changes: 67 additions & 4 deletions modules/sdk-coin-apt/src/lib/transaction/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,12 @@ import {
SignedTransaction,
SimpleTransaction,
TransactionAuthenticatorFeePayer,
TransactionPayload,
} from '@aptos-labs/ts-sdk';
import { DEFAULT_GAS_UNIT_PRICE, SECONDS_PER_WEEK, UNAVAILABLE_TEXT } from '../constants';
import utils from '../utils';
import BigNumber from 'bignumber.js';
import { AptTransactionExplanation } from '../iface';
import { AptTransactionExplanation, TxData } from '../iface';

export abstract class Transaction extends BaseTransaction {
protected _rawTransaction: RawTransaction;
Expand All @@ -42,6 +43,7 @@ export abstract class Transaction extends BaseTransaction {
protected _gasUsed: number;
protected _expirationTime: number;
protected _feePayerAddress: string;
protected _assetId: string;

static EMPTY_PUBLIC_KEY = Buffer.alloc(32);
static EMPTY_SIGNATURE = Buffer.alloc(64);
Expand All @@ -54,6 +56,7 @@ export abstract class Transaction extends BaseTransaction {
this._expirationTime = Math.floor(Date.now() / 1e3) + SECONDS_PER_WEEK;
this._sequenceNumber = 0;
this._sender = AccountAddress.ZERO.toString();
this._assetId = AccountAddress.ZERO.toString();
this._senderSignature = {
publicKey: {
pub: Hex.fromHexInput(Transaction.EMPTY_PUBLIC_KEY).toString(),
Expand Down Expand Up @@ -139,6 +142,45 @@ export abstract class Transaction extends BaseTransaction {
this._type = transactionType;
}

get assetId(): string {
return this._assetId;
}

set assetId(value: string) {
this._assetId = value;
}

protected abstract buildRawTransaction(): void;

protected abstract parseTransactionPayload(payload: TransactionPayload): void;

fromDeserializedSignedTransaction(signedTxn: SignedTransaction): void {
try {
const rawTxn = signedTxn.raw_txn;
this.parseTransactionPayload(rawTxn.payload);
this._sender = rawTxn.sender.toString();
this._sequenceNumber = utils.castToNumber(rawTxn.sequence_number);
this._maxGasAmount = utils.castToNumber(rawTxn.max_gas_amount);
this._gasUnitPrice = utils.castToNumber(rawTxn.gas_unit_price);
this._expirationTime = utils.castToNumber(rawTxn.expiration_timestamp_secs);
this._rawTransaction = rawTxn;

this.loadInputsAndOutputs();
const authenticator = signedTxn.authenticator as TransactionAuthenticatorFeePayer;
this._feePayerAddress = authenticator.fee_payer.address.toString();
const senderAuthenticator = authenticator.sender as AccountAuthenticatorEd25519;
const senderSignature = Buffer.from(senderAuthenticator.signature.toUint8Array());
this.addSenderSignature({ pub: senderAuthenticator.public_key.toString() }, senderSignature);

const feePayerAuthenticator = authenticator.fee_payer.authenticator as AccountAuthenticatorEd25519;
const feePayerSignature = Buffer.from(feePayerAuthenticator.signature.toUint8Array());
this.addFeePayerSignature({ pub: feePayerAuthenticator.public_key.toString() }, feePayerSignature);
} catch (e) {
console.error('invalid signed transaction', e);
throw new Error('invalid signed transaction');
}
}

canSign(_key: BaseKey): boolean {
return false;
}
Expand Down Expand Up @@ -212,8 +254,16 @@ export abstract class Transaction extends BaseTransaction {
];
}

abstract fromRawTransaction(rawTransaction: string): void;

fromRawTransaction(rawTransaction: string): void {
let signedTxn: SignedTransaction;
try {
signedTxn = utils.deserializeSignedTransaction(rawTransaction);
} catch (e) {
console.error('invalid raw transaction', e);
throw new Error('invalid raw transaction');
}
this.fromDeserializedSignedTransaction(signedTxn);
}
/**
* Deserializes a signed transaction hex string
* @param {string} signedRawTransaction
Expand All @@ -228,7 +278,20 @@ export abstract class Transaction extends BaseTransaction {
}
}

protected abstract buildRawTransaction(): void;
toJson(): TxData {
return {
id: this.id,
sender: this.sender,
recipient: this.recipient,
sequenceNumber: this.sequenceNumber,
maxGasAmount: this.maxGasAmount,
gasUnitPrice: this.gasUnitPrice,
gasUsed: this.gasUsed,
expirationTime: this.expirationTime,
feePayer: this.feePayerAddress,
assetId: this.assetId,
};
}

public getFee(): string {
return new BigNumber(this.gasUsed).multipliedBy(this.gasUnitPrice).toString();
Expand Down
71 changes: 15 additions & 56 deletions modules/sdk-coin-apt/src/lib/transaction/transferTransaction.ts
Original file line number Diff line number Diff line change
@@ -1,74 +1,33 @@
import { Transaction } from './transaction';
import { TransferTxData } from '../iface';
import { TransactionType } from '@bitgo/sdk-core';
import { InvalidTransactionError, TransactionRecipient, TransactionType } from '@bitgo/sdk-core';
import {
AccountAddress,
AccountAuthenticatorEd25519,
Aptos,
AptosConfig,
Network,
SignedTransaction,
TransactionAuthenticatorFeePayer,
TransactionPayload,
TransactionPayloadEntryFunction,
} from '@aptos-labs/ts-sdk';
import utils from '../utils';
import { NetworkType } from '@bitgo/statics';

import { BaseCoin as CoinConfig, NetworkType } from '@bitgo/statics';

export class TransferTransaction extends Transaction {
constructor(coinConfig) {
constructor(coinConfig: Readonly<CoinConfig>) {
super(coinConfig);
this._type = TransactionType.Send;
}

toJson(): TransferTxData {
return {
id: this.id,
sender: this.sender,
recipient: this.recipient,
sequenceNumber: this.sequenceNumber,
maxGasAmount: this.maxGasAmount,
gasUnitPrice: this.gasUnitPrice,
gasUsed: this.gasUsed,
expirationTime: this.expirationTime,
feePayer: this.feePayerAddress,
};
}

fromRawTransaction(rawTransaction: string): void {
let signedTxn: SignedTransaction;
try {
signedTxn = utils.deserializeSignedTransaction(rawTransaction);
} catch (e) {
console.error('invalid raw transaction', e);
throw new Error('invalid raw transaction');
protected parseTransactionPayload(payload: TransactionPayload): void {
if (!(payload instanceof TransactionPayloadEntryFunction)) {
throw new InvalidTransactionError('Invalid transaction payload');
}
this.fromDeserializedSignedTransaction(signedTxn);
}

fromDeserializedSignedTransaction(signedTxn: SignedTransaction): void {
try {
const rawTxn = signedTxn.raw_txn;
this._sender = rawTxn.sender.toString();
this._recipient = utils.getRecipientFromTransactionPayload(rawTxn.payload);
this._sequenceNumber = utils.castToNumber(rawTxn.sequence_number);
this._maxGasAmount = utils.castToNumber(rawTxn.max_gas_amount);
this._gasUnitPrice = utils.castToNumber(rawTxn.gas_unit_price);
this._expirationTime = utils.castToNumber(rawTxn.expiration_timestamp_secs);
this._rawTransaction = rawTxn;

this.loadInputsAndOutputs();
const authenticator = signedTxn.authenticator as TransactionAuthenticatorFeePayer;
this._feePayerAddress = authenticator.fee_payer.address.toString();
const senderAuthenticator = authenticator.sender as AccountAuthenticatorEd25519;
const senderSignature = Buffer.from(senderAuthenticator.signature.toUint8Array());
this.addSenderSignature({ pub: senderAuthenticator.public_key.toString() }, senderSignature);

const feePayerAuthenticator = authenticator.fee_payer.authenticator as AccountAuthenticatorEd25519;
const feePayerSignature = Buffer.from(feePayerAuthenticator.signature.toUint8Array());
this.addFeePayerSignature({ pub: feePayerAuthenticator.public_key.toString() }, feePayerSignature);
} catch (e) {
console.error('invalid signed transaction', e);
throw new Error('invalid signed transaction');
const entryFunction = payload.entryFunction;
if (!this._recipient) {
this._recipient = {} as TransactionRecipient;
}
this._recipient.address = entryFunction.args[0].toString();
const amountBuffer = Buffer.from(entryFunction.args[1].bcsToBytes());
this._recipient.amount = amountBuffer.readBigUint64LE().toString();
}

protected async buildRawTransaction(): Promise<void> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { TransactionBuilder } from './transactionBuilder';
import { BaseCoin as CoinConfig } from '@bitgo/statics';
import { FungibleAssetTransaction } from '../transaction/fungibleAssetTransaction';
import { TransactionType } from '@bitgo/sdk-core';
import BigNumber from 'bignumber.js';
import utils from '../utils';
import { TransactionPayload, TransactionPayloadEntryFunction } from '@aptos-labs/ts-sdk';
import { FUNGIBLE_ASSET } from '../constants';

export class FungibleAssetTransactionBuilder extends TransactionBuilder {
constructor(_coinConfig: Readonly<CoinConfig>) {
super(_coinConfig);
this._transaction = new FungibleAssetTransaction(_coinConfig);
}

protected get transactionType(): TransactionType {
return TransactionType.SendToken;
}

/** @inheritdoc */
validateTransaction(transaction?: FungibleAssetTransaction): void {
if (!transaction) {
throw new Error('fungible asset transaction not defined');
}
super.validateTransaction(transaction);
this.validateAddress({ address: transaction.assetId });
}

protected isValidTransactionPayload(payload: TransactionPayload) {
try {
if (
!(payload instanceof TransactionPayloadEntryFunction) ||
payload.entryFunction.args.length !== 3 ||
payload.entryFunction.type_args.length === 0 ||
FUNGIBLE_ASSET !== payload.entryFunction.type_args[0].toString()
) {
console.error('invalid transaction payload');
return false;
}
const entryFunction = payload.entryFunction;
const fungibleTokenAddress = entryFunction.args[0].toString();
const recipientAddress = entryFunction.args[1].toString();
const amountBuffer = Buffer.from(entryFunction.args[2].bcsToBytes());
const recipientAmount = new BigNumber(amountBuffer.readBigUint64LE().toString());
return (
utils.isValidAddress(recipientAddress) &&
utils.isValidAddress(fungibleTokenAddress) &&
!recipientAmount.isLessThan(0)
);
} catch (e) {
console.error('invalid transaction payload', e);
return false;
}
}
}
Loading

0 comments on commit d8b7424

Please sign in to comment.