Skip to content

Commit 6857221

Browse files
committed
Merge pull request #5323 from BitGo/coin-2258-apt-tx-builder-3
feat(sdk-coin-apt): transaction builder
2 parents 48d7edf + 87df169 commit 6857221

File tree

10 files changed

+370
-49
lines changed

10 files changed

+370
-49
lines changed

modules/sdk-coin-apt/src/lib/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export const APT_ADDRESS_LENGTH = 64;
22
export const APT_TRANSACTION_ID_LENGTH = 64;
33
export const APT_BLOCK_ID_LENGTH = 64;
44
export const APT_SIGNATURE_LENGTH = 128;
5+
export const UNAVAILABLE_TEXT = 'UNAVAILABLE';

modules/sdk-coin-apt/src/lib/iface.ts

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,17 @@
1-
import {
2-
TransactionExplanation as BaseTransactionExplanation,
3-
TransactionType as BitGoTransactionType,
4-
} from '@bitgo/sdk-core';
1+
import { ITransactionExplanation, TransactionFee } from '@bitgo/sdk-core';
52

6-
export enum AptTransactionType {
7-
Transfer = 'Transfer',
8-
TokenTransfer = 'TokenTransfer',
9-
}
10-
11-
export interface TransactionExplanation extends BaseTransactionExplanation {
12-
type: BitGoTransactionType;
13-
}
3+
export type TransactionExplanation = ITransactionExplanation<TransactionFee>;
144

155
/**
166
* The transaction data returned from the toJson() function of a transaction
177
*/
188
export interface TxData {
199
id: string;
2010
sender: string;
21-
sequenceNumber: BigInt;
22-
maxGasAmount: BigInt;
23-
gasUnitPrice: BigInt;
24-
expirationTime: BigInt;
11+
sequenceNumber: bigint;
12+
maxGasAmount: bigint;
13+
gasUnitPrice: bigint;
14+
expirationTime: bigint;
2515
payload: AptPayload;
2616
chainId: number;
2717
}

modules/sdk-coin-apt/src/lib/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import * as Utils from './utils';
22
import * as Interface from './iface';
33

44
export { KeyPair } from './keyPair';
5-
export { Transaction } from './transaction';
5+
export { Transaction } from './transaction/transaction';
6+
export { TransferTransaction } from './transaction/transferTransaction';
67
export { TransactionBuilder } from './transactionBuilder';
78
export { TransferBuilder } from './transferBuilder';
89
export { TransactionBuilderFactory } from './transactionBuilderFactory';

modules/sdk-coin-apt/src/lib/transaction.ts

Lines changed: 0 additions & 16 deletions
This file was deleted.
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import {
2+
BaseKey,
3+
BaseTransaction,
4+
InvalidTransactionError,
5+
PublicKey,
6+
Signature,
7+
TransactionRecipient,
8+
TransactionType,
9+
} from '@bitgo/sdk-core';
10+
import { TransactionExplanation, TxData } from '../iface';
11+
import { BaseCoin as CoinConfig } from '@bitgo/statics';
12+
import {
13+
AccountAddress,
14+
AccountAuthenticatorEd25519,
15+
Ed25519PublicKey,
16+
Ed25519Signature,
17+
generateUserTransactionHash,
18+
RawTransaction,
19+
SignedTransaction,
20+
SimpleTransaction,
21+
TransactionAuthenticatorEd25519,
22+
} from '@aptos-labs/ts-sdk';
23+
import { UNAVAILABLE_TEXT } from '../constants';
24+
import utils from '../utils';
25+
26+
export abstract class Transaction extends BaseTransaction {
27+
protected _rawTransaction: RawTransaction;
28+
protected _signature: Signature;
29+
30+
static DEFAULT_PUBLIC_KEY = Buffer.alloc(32);
31+
static DEFAULT_SIGNATURE = Buffer.alloc(64);
32+
33+
constructor(coinConfig: Readonly<CoinConfig>) {
34+
super(coinConfig);
35+
}
36+
37+
/** @inheritDoc **/
38+
public get id(): string {
39+
return this._id ?? UNAVAILABLE_TEXT;
40+
}
41+
42+
set transactionType(transactionType: TransactionType) {
43+
this._type = transactionType;
44+
}
45+
46+
public get signablePayload(): Buffer {
47+
const rawTxnHex = this._rawTransaction.bcsToHex().toString();
48+
return Buffer.from(rawTxnHex, 'hex');
49+
}
50+
51+
public get sender(): string {
52+
return this._rawTransaction.sender.toString();
53+
}
54+
55+
set sender(senderAddress: string) {
56+
// Cannot assign to 'sender' because it is a read-only property in RawTransaction.
57+
const { sequence_number, payload, max_gas_amount, gas_unit_price, expiration_timestamp_secs, chain_id } =
58+
this._rawTransaction;
59+
this._rawTransaction = new RawTransaction(
60+
AccountAddress.fromString(senderAddress),
61+
sequence_number,
62+
payload,
63+
max_gas_amount,
64+
gas_unit_price,
65+
expiration_timestamp_secs,
66+
chain_id
67+
);
68+
}
69+
70+
public get recipient(): TransactionRecipient {
71+
return utils.getRecipientFromTransactionPayload(this._rawTransaction.payload);
72+
}
73+
74+
canSign(_key: BaseKey): boolean {
75+
return false;
76+
}
77+
78+
toBroadcastFormat(): string {
79+
if (!this._rawTransaction) {
80+
throw new InvalidTransactionError('Empty transaction');
81+
}
82+
return this.serialize();
83+
}
84+
85+
serialize(): string {
86+
let publicKeyBuffer = Transaction.DEFAULT_PUBLIC_KEY;
87+
let signatureBuffer = Transaction.DEFAULT_SIGNATURE;
88+
if (this._signature && this._signature.publicKey && this._signature.signature) {
89+
publicKeyBuffer = Buffer.from(this._signature.publicKey.pub, 'hex');
90+
signatureBuffer = this._signature.signature;
91+
}
92+
const publicKey = new Ed25519PublicKey(publicKeyBuffer);
93+
const signature = new Ed25519Signature(signatureBuffer);
94+
const txnAuthenticator = new TransactionAuthenticatorEd25519(publicKey, signature);
95+
const signedTxn = new SignedTransaction(this._rawTransaction, txnAuthenticator);
96+
return signedTxn.toString();
97+
}
98+
99+
abstract toJson(): TxData;
100+
101+
addSignature(publicKey: PublicKey, signature: Buffer): void {
102+
const publicKeyBuffer = Buffer.from(publicKey.pub, 'hex');
103+
if (!Transaction.DEFAULT_PUBLIC_KEY.equals(publicKeyBuffer) && !Transaction.DEFAULT_SIGNATURE.equals(signature)) {
104+
this._signatures.push(signature.toString('hex'));
105+
this._signature = { publicKey, signature };
106+
this.serialize();
107+
}
108+
}
109+
110+
async build(): Promise<void> {
111+
this.loadInputsAndOutputs();
112+
if (this._signature && this._signature.publicKey && this._signature.signature) {
113+
const transaction = new SimpleTransaction(this._rawTransaction);
114+
const publicKey = new Ed25519PublicKey(Buffer.from(this._signature.publicKey.pub, 'hex'));
115+
const signature = new Ed25519Signature(this._signature.signature);
116+
const senderAuthenticator = new AccountAuthenticatorEd25519(publicKey, signature);
117+
this._id = generateUserTransactionHash({ transaction, senderAuthenticator });
118+
}
119+
}
120+
121+
loadInputsAndOutputs(): void {
122+
const txRecipient = this.recipient;
123+
this._inputs = [
124+
{
125+
address: this.sender,
126+
value: txRecipient.amount as string,
127+
coin: this._coinConfig.name,
128+
},
129+
];
130+
this._outputs = [
131+
{
132+
address: txRecipient.address,
133+
value: txRecipient.amount as string,
134+
coin: this._coinConfig.name,
135+
},
136+
];
137+
}
138+
139+
fromRawTransaction(rawTransaction: string): void {
140+
try {
141+
const signedTxn = utils.deserializeSignedTransaction(rawTransaction);
142+
this._rawTransaction = signedTxn.raw_txn;
143+
144+
this.loadInputsAndOutputs();
145+
146+
const authenticator = signedTxn.authenticator as TransactionAuthenticatorEd25519;
147+
const publicKey = Buffer.from(authenticator.public_key.toUint8Array());
148+
const signature = Buffer.from(authenticator.signature.toUint8Array());
149+
this.addSignature({ pub: publicKey.toString() }, signature);
150+
} catch (e) {
151+
console.error('invalid raw transaction', e);
152+
throw new Error('invalid raw transaction');
153+
}
154+
}
155+
156+
/** @inheritDoc */
157+
explainTransaction(): TransactionExplanation {
158+
const displayOrder = ['id', 'outputs', 'outputAmount', 'changeOutputs', 'changeAmount', 'fee', 'withdrawAmount'];
159+
160+
const outputs: TransactionRecipient[] = [this.recipient];
161+
const outputAmount = outputs[0].amount;
162+
return {
163+
displayOrder,
164+
id: this.id,
165+
outputs,
166+
outputAmount,
167+
changeOutputs: [],
168+
changeAmount: '0',
169+
fee: { fee: 'UNKNOWN' },
170+
};
171+
}
172+
173+
static deserializeRawTransaction(rawTransaction: string): RawTransaction {
174+
try {
175+
return utils.deserializeRawTransaction(rawTransaction);
176+
} catch (e) {
177+
console.error('invalid raw transaction', e);
178+
throw new Error('invalid raw transaction');
179+
}
180+
}
181+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { Transaction } from './transaction';
2+
import { TxData } from '../iface';
3+
import { TransactionPayloadEntryFunction } from '@aptos-labs/ts-sdk';
4+
5+
export class TransferTransaction extends Transaction {
6+
toJson(): TxData {
7+
const rawTxn = this._rawTransaction;
8+
const payload = this._rawTransaction.payload as TransactionPayloadEntryFunction;
9+
const entryFunction = payload.entryFunction;
10+
return {
11+
id: this.id,
12+
sender: this.sender,
13+
sequenceNumber: rawTxn.sequence_number,
14+
maxGasAmount: rawTxn.max_gas_amount,
15+
gasUnitPrice: rawTxn.gas_unit_price,
16+
expirationTime: rawTxn.expiration_timestamp_secs,
17+
payload: {
18+
function: entryFunction.function_name.identifier,
19+
typeArguments: entryFunction.type_args.map((a) => a.toString()),
20+
arguments: entryFunction.args.map((a) => a.toString()),
21+
type: 'entry_function_payload',
22+
},
23+
chainId: rawTxn.chain_id.chainId,
24+
};
25+
}
26+
}

modules/sdk-coin-apt/src/lib/transactionBuilder.ts

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,24 @@ import {
33
BaseKey,
44
BaseTransactionBuilder,
55
BuildTransactionError,
6-
FeeOptions,
6+
ParseTransactionError,
77
PublicKey as BasePublicKey,
88
Signature,
99
TransactionType,
1010
} from '@bitgo/sdk-core';
11-
import { Transaction } from './transaction';
11+
import { Transaction } from './transaction/transaction';
1212
import utils from './utils';
1313
import BigNumber from 'bignumber.js';
14+
import { BaseCoin as CoinConfig } from '@bitgo/statics';
1415

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

20+
constructor(coinConfig: Readonly<CoinConfig>) {
21+
super(coinConfig);
22+
}
23+
1924
// get and set region
2025
/**
2126
* The transaction type.
@@ -40,6 +45,7 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder {
4045
/** @inheritDoc */
4146
addSignature(publicKey: BasePublicKey, signature: Buffer): void {
4247
this._signatures.push({ publicKey, signature });
48+
this.transaction.addSignature(publicKey, signature);
4349
}
4450

4551
/**
@@ -50,23 +56,32 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder {
5056
* @returns {TransactionBuilder} This transaction builder
5157
*/
5258
sender(senderAddress: string): this {
53-
throw new Error('Method not implemented.');
54-
}
55-
56-
fee(feeOptions: FeeOptions): this {
57-
throw new Error('Method not implemented.');
59+
this.validateAddress({ address: senderAddress });
60+
this._transaction.sender = senderAddress;
61+
return this;
5862
}
5963

6064
/** @inheritdoc */
6165
protected fromImplementation(rawTransaction: string): Transaction {
62-
throw new Error('Method not implemented.');
66+
this.transaction.fromRawTransaction(rawTransaction);
67+
this.transaction.transactionType = this.transactionType;
68+
return this.transaction;
6369
}
6470

6571
/** @inheritdoc */
6672
protected async buildImplementation(): Promise<Transaction> {
67-
throw new Error('Method not implemented.');
73+
this.transaction.transactionType = this.transactionType;
74+
await this.transaction.build();
75+
return this.transaction;
6876
}
6977

78+
/**
79+
* Initialize the transaction builder fields using the decoded transaction data
80+
*
81+
* @param {Transaction} tx the transaction data
82+
*/
83+
abstract initBuilder(tx: Transaction): void;
84+
7085
// region Validators
7186
/** @inheritdoc */
7287
validateAddress(address: BaseAddress, addressFormat?: string): void {
@@ -82,12 +97,20 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder {
8297

8398
/** @inheritdoc */
8499
validateRawTransaction(rawTransaction: string): void {
85-
throw new Error('Method not implemented.');
100+
if (!rawTransaction) {
101+
throw new ParseTransactionError('Invalid raw transaction: Undefined');
102+
}
103+
if (!utils.isValidRawTransaction(rawTransaction)) {
104+
throw new ParseTransactionError('Invalid raw transaction');
105+
}
86106
}
87107

88108
/** @inheritdoc */
89109
validateTransaction(transaction?: Transaction): void {
90-
throw new Error('Method not implemented.');
110+
if (!transaction) {
111+
throw new Error('transaction not defined');
112+
}
113+
this.validateRawTransaction(transaction.toBroadcastFormat());
91114
}
92115

93116
/** @inheritdoc */

0 commit comments

Comments
 (0)