Skip to content

Commit 394bbee

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 394bbee

File tree

10 files changed

+341
-45
lines changed

10 files changed

+341
-45
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: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,6 @@
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

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