Skip to content

Commit 4fa814a

Browse files
committed
feat(sdk-coin-apt): transaction builder
TICKET: COIN-2258
1 parent 4ae6e32 commit 4fa814a

File tree

12 files changed

+337
-50
lines changed

12 files changed

+337
-50
lines changed

modules/sdk-coin-apt/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,10 @@
4242
"dependencies": {
4343
"@aptos-labs/ts-sdk": "^1.32.0",
4444
"@bitgo/sdk-core": "^28.18.0",
45+
"@bitgo/sdk-lib-mpc": "^10.1.0",
4546
"@bitgo/statics": "^50.17.0",
46-
"bignumber.js": "^9.1.2"
47+
"bignumber.js": "^9.1.2",
48+
"lodash": "^4.17.21"
4749
},
4850
"devDependencies": {
4951
"@bitgo/sdk-api": "^1.57.6",

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: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
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+
this._rawTransaction.sender = AccountAddress.fromString(senderAddress);
56+
}
57+
58+
public get recipient(): TransactionRecipient {
59+
return utils.getRecipientFromTransactionPayload(this._rawTransaction.payload);
60+
}
61+
62+
canSign(_key: BaseKey): boolean {
63+
return false;
64+
}
65+
66+
toBroadcastFormat(): string {
67+
if (!this._rawTransaction) {
68+
throw new InvalidTransactionError('Empty transaction');
69+
}
70+
return this.serialize();
71+
}
72+
73+
serialize(): string {
74+
if (!this._signature || !this._signature.publicKey || !this._signature.signature) {
75+
return this._rawTransaction.bcsToHex().toString();
76+
}
77+
const publicKey = new Ed25519PublicKey(Buffer.from(this._signature.publicKey.pub, 'hex'));
78+
const signature = new Ed25519Signature(this._signature.signature);
79+
const txnAuthenticator = new TransactionAuthenticatorEd25519(publicKey, signature);
80+
const signedTxn = new SignedTransaction(this._rawTransaction, txnAuthenticator);
81+
return signedTxn.bcsToHex().toString();
82+
}
83+
84+
abstract toJson(): TxData;
85+
86+
addSignature(publicKey: PublicKey, signature: Buffer): void {
87+
this._signatures.push(signature.toString('hex'));
88+
this._signature = { publicKey, signature };
89+
this.serialize();
90+
}
91+
92+
async build(): Promise<void> {
93+
this.loadInputsAndOutputs();
94+
if (this._signature) {
95+
const publicKey = new Ed25519PublicKey(Buffer.from(this._signature.publicKey.pub, 'hex'));
96+
const signature = new Ed25519Signature(this._signature.signature);
97+
98+
this._id = generateUserTransactionHash({
99+
transaction: new SimpleTransaction(this._rawTransaction),
100+
senderAuthenticator: new AccountAuthenticatorEd25519(publicKey, signature),
101+
});
102+
}
103+
}
104+
105+
loadInputsAndOutputs(): void {
106+
const txRecipient = this.recipient;
107+
this._inputs = [
108+
{
109+
address: this.sender,
110+
value: txRecipient.amount as string,
111+
coin: this._coinConfig.name,
112+
},
113+
];
114+
this._outputs = [
115+
{
116+
address: txRecipient.address,
117+
value: txRecipient.amount as string,
118+
coin: this._coinConfig.name,
119+
},
120+
];
121+
}
122+
123+
fromRawTransaction(rawTransaction: string): void {
124+
try {
125+
const txnBytes = Hex.fromHexString(rawTransaction).toUint8Array();
126+
const deserializer = new Deserializer(txnBytes);
127+
this._rawTransaction = deserializer.deserialize(RawTransaction);
128+
129+
this.loadInputsAndOutputs();
130+
} catch (e) {
131+
console.error('invalid raw transaction', e);
132+
throw new Error('invalid raw transaction');
133+
}
134+
}
135+
136+
/** @inheritDoc */
137+
explainTransaction(): TransactionExplanation {
138+
const displayOrder = ['id', 'outputs', 'outputAmount', 'changeOutputs', 'changeAmount', 'fee', 'withdrawAmount'];
139+
140+
const outputs: TransactionRecipient[] = [this.recipient];
141+
const outputAmount = outputs[0].amount;
142+
return {
143+
displayOrder,
144+
id: this.id,
145+
outputs,
146+
outputAmount,
147+
changeOutputs: [],
148+
changeAmount: '0',
149+
fee: { fee: 'UNKNOWN' },
150+
};
151+
}
152+
153+
static deserializeRawTransaction(rawTransaction: string): RawTransaction {
154+
try {
155+
const txnBytes = Hex.fromHexString(rawTransaction).toUint8Array();
156+
const deserializer = new Deserializer(txnBytes);
157+
return deserializer.deserialize(RawTransaction);
158+
} catch (e) {
159+
console.error('invalid raw transaction', e);
160+
throw new Error('invalid raw transaction');
161+
}
162+
}
163+
}
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)