Skip to content

Commit 64a259e

Browse files
authored
Merge pull request #5431 from BitGo/COIN-2894-fungible-token-transfer
feat(sdk-coin-apt): fungible token transfer
2 parents b480997 + 65cbd70 commit 64a259e

File tree

17 files changed

+522
-188
lines changed

17 files changed

+522
-188
lines changed

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,8 @@ export const DEFAULT_GAS_UNIT_PRICE = 100;
77
export const SECONDS_PER_WEEK = 7 * 24 * 60 * 60; // Days * Hours * Minutes * Seconds
88

99
export const APTOS_ACCOUNT_MODULE = 'aptos_account';
10-
export const FUNGIBLE_ASSET_MODULE = 'fungible_asset';
10+
export const FUNGIBLE_ASSET_MODULE = 'primary_fungible_store';
11+
12+
export const FUNGIBLE_ASSET_FUNCTION = '0x1::primary_fungible_store::transfer';
13+
14+
export const FUNGIBLE_ASSET = '0x1::fungible_asset::Metadata';

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export interface AptTransactionExplanation extends BaseTransactionExplanation {
1212
/**
1313
* The transaction data returned from the toJson() function of a transaction
1414
*/
15-
export interface TransferTxData {
15+
export interface TxData {
1616
id: string;
1717
sender: string;
1818
recipient: TransactionRecipient;
@@ -22,4 +22,5 @@ export interface TransferTxData {
2222
gasUsed: number;
2323
expirationTime: number;
2424
feePayer: string;
25+
assetId: string;
2526
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import * as Interface from './iface';
44
export { KeyPair } from './keyPair';
55
export { Transaction } from './transaction/transaction';
66
export { TransferTransaction } from './transaction/transferTransaction';
7-
export { TransactionBuilder } from './transactionBuilder';
8-
export { TransferBuilder } from './transferBuilder';
7+
export { TransactionBuilder } from './transactionBuilder/transactionBuilder';
8+
export { TransferBuilder } from './transactionBuilder/transferBuilder';
99
export { TransactionBuilderFactory } from './transactionBuilderFactory';
1010
export { Interface, Utils };
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { Transaction } from './transaction';
2+
import {
3+
AccountAddress,
4+
Aptos,
5+
AptosConfig,
6+
EntryFunctionABI,
7+
Network,
8+
parseTypeTag,
9+
TransactionPayload,
10+
TransactionPayloadEntryFunction,
11+
TypeTagAddress,
12+
TypeTagU64,
13+
} from '@aptos-labs/ts-sdk';
14+
import { InvalidTransactionError, TransactionRecipient, TransactionType } from '@bitgo/sdk-core';
15+
import { BaseCoin as CoinConfig, NetworkType } from '@bitgo/statics';
16+
import { FUNGIBLE_ASSET, FUNGIBLE_ASSET_FUNCTION } from '../constants';
17+
18+
export class FungibleAssetTransaction extends Transaction {
19+
constructor(coinConfig: Readonly<CoinConfig>) {
20+
super(coinConfig);
21+
this._type = TransactionType.SendToken;
22+
}
23+
24+
protected parseTransactionPayload(payload: TransactionPayload): void {
25+
if (
26+
!(payload instanceof TransactionPayloadEntryFunction) ||
27+
payload.entryFunction.args.length !== 3 ||
28+
payload.entryFunction.type_args.length !== 1 ||
29+
FUNGIBLE_ASSET !== payload.entryFunction.type_args[0].toString()
30+
) {
31+
throw new InvalidTransactionError('Invalid transaction payload');
32+
}
33+
const entryFunction = payload.entryFunction;
34+
if (!this._recipient) {
35+
this._recipient = {} as TransactionRecipient;
36+
}
37+
this._assetId = entryFunction.args[0].toString();
38+
this._recipient.address = entryFunction.args[1].toString();
39+
const amountBuffer = Buffer.from(entryFunction.args[2].bcsToBytes());
40+
this._recipient.amount = amountBuffer.readBigUint64LE().toString();
41+
}
42+
43+
protected async buildRawTransaction(): Promise<void> {
44+
const network: Network = this._coinConfig.network.type === NetworkType.MAINNET ? Network.MAINNET : Network.TESTNET;
45+
const aptos = new Aptos(new AptosConfig({ network }));
46+
const senderAddress = AccountAddress.fromString(this._sender);
47+
const recipientAddress = AccountAddress.fromString(this._recipient.address);
48+
const fungibleTokenAddress = this._assetId;
49+
50+
const faTransferAbi: EntryFunctionABI = {
51+
typeParameters: [{ constraints: [] }],
52+
parameters: [parseTypeTag('0x1::object::Object'), new TypeTagAddress(), new TypeTagU64()],
53+
};
54+
55+
const simpleTxn = await aptos.transaction.build.simple({
56+
sender: senderAddress,
57+
data: {
58+
function: FUNGIBLE_ASSET_FUNCTION,
59+
typeArguments: [FUNGIBLE_ASSET],
60+
functionArguments: [fungibleTokenAddress, recipientAddress, this.recipient.amount],
61+
abi: faTransferAbi,
62+
},
63+
options: {
64+
maxGasAmount: this.maxGasAmount,
65+
gasUnitPrice: this.gasUnitPrice,
66+
expireTimestamp: this.expirationTime,
67+
accountSequenceNumber: this.sequenceNumber,
68+
},
69+
});
70+
this._rawTransaction = simpleTxn.rawTransaction;
71+
}
72+
}

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

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,12 @@ import {
2424
SignedTransaction,
2525
SimpleTransaction,
2626
TransactionAuthenticatorFeePayer,
27+
TransactionPayload,
2728
} from '@aptos-labs/ts-sdk';
2829
import { DEFAULT_GAS_UNIT_PRICE, SECONDS_PER_WEEK, UNAVAILABLE_TEXT } from '../constants';
2930
import utils from '../utils';
3031
import BigNumber from 'bignumber.js';
31-
import { AptTransactionExplanation } from '../iface';
32+
import { AptTransactionExplanation, TxData } from '../iface';
3233

3334
export abstract class Transaction extends BaseTransaction {
3435
protected _rawTransaction: RawTransaction;
@@ -42,6 +43,7 @@ export abstract class Transaction extends BaseTransaction {
4243
protected _gasUsed: number;
4344
protected _expirationTime: number;
4445
protected _feePayerAddress: string;
46+
protected _assetId: string;
4547

4648
static EMPTY_PUBLIC_KEY = Buffer.alloc(32);
4749
static EMPTY_SIGNATURE = Buffer.alloc(64);
@@ -54,6 +56,7 @@ export abstract class Transaction extends BaseTransaction {
5456
this._expirationTime = Math.floor(Date.now() / 1e3) + SECONDS_PER_WEEK;
5557
this._sequenceNumber = 0;
5658
this._sender = AccountAddress.ZERO.toString();
59+
this._assetId = AccountAddress.ZERO.toString();
5760
this._senderSignature = {
5861
publicKey: {
5962
pub: Hex.fromHexInput(Transaction.EMPTY_PUBLIC_KEY).toString(),
@@ -139,6 +142,45 @@ export abstract class Transaction extends BaseTransaction {
139142
this._type = transactionType;
140143
}
141144

145+
get assetId(): string {
146+
return this._assetId;
147+
}
148+
149+
set assetId(value: string) {
150+
this._assetId = value;
151+
}
152+
153+
protected abstract buildRawTransaction(): void;
154+
155+
protected abstract parseTransactionPayload(payload: TransactionPayload): void;
156+
157+
fromDeserializedSignedTransaction(signedTxn: SignedTransaction): void {
158+
try {
159+
const rawTxn = signedTxn.raw_txn;
160+
this.parseTransactionPayload(rawTxn.payload);
161+
this._sender = rawTxn.sender.toString();
162+
this._sequenceNumber = utils.castToNumber(rawTxn.sequence_number);
163+
this._maxGasAmount = utils.castToNumber(rawTxn.max_gas_amount);
164+
this._gasUnitPrice = utils.castToNumber(rawTxn.gas_unit_price);
165+
this._expirationTime = utils.castToNumber(rawTxn.expiration_timestamp_secs);
166+
this._rawTransaction = rawTxn;
167+
168+
this.loadInputsAndOutputs();
169+
const authenticator = signedTxn.authenticator as TransactionAuthenticatorFeePayer;
170+
this._feePayerAddress = authenticator.fee_payer.address.toString();
171+
const senderAuthenticator = authenticator.sender as AccountAuthenticatorEd25519;
172+
const senderSignature = Buffer.from(senderAuthenticator.signature.toUint8Array());
173+
this.addSenderSignature({ pub: senderAuthenticator.public_key.toString() }, senderSignature);
174+
175+
const feePayerAuthenticator = authenticator.fee_payer.authenticator as AccountAuthenticatorEd25519;
176+
const feePayerSignature = Buffer.from(feePayerAuthenticator.signature.toUint8Array());
177+
this.addFeePayerSignature({ pub: feePayerAuthenticator.public_key.toString() }, feePayerSignature);
178+
} catch (e) {
179+
console.error('invalid signed transaction', e);
180+
throw new Error('invalid signed transaction');
181+
}
182+
}
183+
142184
canSign(_key: BaseKey): boolean {
143185
return false;
144186
}
@@ -212,8 +254,16 @@ export abstract class Transaction extends BaseTransaction {
212254
];
213255
}
214256

215-
abstract fromRawTransaction(rawTransaction: string): void;
216-
257+
fromRawTransaction(rawTransaction: string): void {
258+
let signedTxn: SignedTransaction;
259+
try {
260+
signedTxn = utils.deserializeSignedTransaction(rawTransaction);
261+
} catch (e) {
262+
console.error('invalid raw transaction', e);
263+
throw new Error('invalid raw transaction');
264+
}
265+
this.fromDeserializedSignedTransaction(signedTxn);
266+
}
217267
/**
218268
* Deserializes a signed transaction hex string
219269
* @param {string} signedRawTransaction
@@ -228,7 +278,20 @@ export abstract class Transaction extends BaseTransaction {
228278
}
229279
}
230280

231-
protected abstract buildRawTransaction(): void;
281+
toJson(): TxData {
282+
return {
283+
id: this.id,
284+
sender: this.sender,
285+
recipient: this.recipient,
286+
sequenceNumber: this.sequenceNumber,
287+
maxGasAmount: this.maxGasAmount,
288+
gasUnitPrice: this.gasUnitPrice,
289+
gasUsed: this.gasUsed,
290+
expirationTime: this.expirationTime,
291+
feePayer: this.feePayerAddress,
292+
assetId: this.assetId,
293+
};
294+
}
232295

233296
public getFee(): string {
234297
return new BigNumber(this.gasUsed).multipliedBy(this.gasUnitPrice).toString();

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

Lines changed: 15 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,74 +1,33 @@
11
import { Transaction } from './transaction';
2-
import { TransferTxData } from '../iface';
3-
import { TransactionType } from '@bitgo/sdk-core';
2+
import { InvalidTransactionError, TransactionRecipient, TransactionType } from '@bitgo/sdk-core';
43
import {
54
AccountAddress,
6-
AccountAuthenticatorEd25519,
75
Aptos,
86
AptosConfig,
97
Network,
10-
SignedTransaction,
11-
TransactionAuthenticatorFeePayer,
8+
TransactionPayload,
9+
TransactionPayloadEntryFunction,
1210
} from '@aptos-labs/ts-sdk';
13-
import utils from '../utils';
14-
import { NetworkType } from '@bitgo/statics';
11+
12+
import { BaseCoin as CoinConfig, NetworkType } from '@bitgo/statics';
1513

1614
export class TransferTransaction extends Transaction {
17-
constructor(coinConfig) {
15+
constructor(coinConfig: Readonly<CoinConfig>) {
1816
super(coinConfig);
1917
this._type = TransactionType.Send;
2018
}
2119

22-
toJson(): TransferTxData {
23-
return {
24-
id: this.id,
25-
sender: this.sender,
26-
recipient: this.recipient,
27-
sequenceNumber: this.sequenceNumber,
28-
maxGasAmount: this.maxGasAmount,
29-
gasUnitPrice: this.gasUnitPrice,
30-
gasUsed: this.gasUsed,
31-
expirationTime: this.expirationTime,
32-
feePayer: this.feePayerAddress,
33-
};
34-
}
35-
36-
fromRawTransaction(rawTransaction: string): void {
37-
let signedTxn: SignedTransaction;
38-
try {
39-
signedTxn = utils.deserializeSignedTransaction(rawTransaction);
40-
} catch (e) {
41-
console.error('invalid raw transaction', e);
42-
throw new Error('invalid raw transaction');
20+
protected parseTransactionPayload(payload: TransactionPayload): void {
21+
if (!(payload instanceof TransactionPayloadEntryFunction)) {
22+
throw new InvalidTransactionError('Invalid transaction payload');
4323
}
44-
this.fromDeserializedSignedTransaction(signedTxn);
45-
}
46-
47-
fromDeserializedSignedTransaction(signedTxn: SignedTransaction): void {
48-
try {
49-
const rawTxn = signedTxn.raw_txn;
50-
this._sender = rawTxn.sender.toString();
51-
this._recipient = utils.getRecipientFromTransactionPayload(rawTxn.payload);
52-
this._sequenceNumber = utils.castToNumber(rawTxn.sequence_number);
53-
this._maxGasAmount = utils.castToNumber(rawTxn.max_gas_amount);
54-
this._gasUnitPrice = utils.castToNumber(rawTxn.gas_unit_price);
55-
this._expirationTime = utils.castToNumber(rawTxn.expiration_timestamp_secs);
56-
this._rawTransaction = rawTxn;
57-
58-
this.loadInputsAndOutputs();
59-
const authenticator = signedTxn.authenticator as TransactionAuthenticatorFeePayer;
60-
this._feePayerAddress = authenticator.fee_payer.address.toString();
61-
const senderAuthenticator = authenticator.sender as AccountAuthenticatorEd25519;
62-
const senderSignature = Buffer.from(senderAuthenticator.signature.toUint8Array());
63-
this.addSenderSignature({ pub: senderAuthenticator.public_key.toString() }, senderSignature);
64-
65-
const feePayerAuthenticator = authenticator.fee_payer.authenticator as AccountAuthenticatorEd25519;
66-
const feePayerSignature = Buffer.from(feePayerAuthenticator.signature.toUint8Array());
67-
this.addFeePayerSignature({ pub: feePayerAuthenticator.public_key.toString() }, feePayerSignature);
68-
} catch (e) {
69-
console.error('invalid signed transaction', e);
70-
throw new Error('invalid signed transaction');
24+
const entryFunction = payload.entryFunction;
25+
if (!this._recipient) {
26+
this._recipient = {} as TransactionRecipient;
7127
}
28+
this._recipient.address = entryFunction.args[0].toString();
29+
const amountBuffer = Buffer.from(entryFunction.args[1].bcsToBytes());
30+
this._recipient.amount = amountBuffer.readBigUint64LE().toString();
7231
}
7332

7433
protected async buildRawTransaction(): Promise<void> {
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { TransactionBuilder } from './transactionBuilder';
2+
import { BaseCoin as CoinConfig } from '@bitgo/statics';
3+
import { FungibleAssetTransaction } from '../transaction/fungibleAssetTransaction';
4+
import { TransactionType } from '@bitgo/sdk-core';
5+
import BigNumber from 'bignumber.js';
6+
import utils from '../utils';
7+
import { TransactionPayload, TransactionPayloadEntryFunction } from '@aptos-labs/ts-sdk';
8+
import { FUNGIBLE_ASSET } from '../constants';
9+
10+
export class FungibleAssetTransactionBuilder extends TransactionBuilder {
11+
constructor(_coinConfig: Readonly<CoinConfig>) {
12+
super(_coinConfig);
13+
this._transaction = new FungibleAssetTransaction(_coinConfig);
14+
}
15+
16+
protected get transactionType(): TransactionType {
17+
return TransactionType.SendToken;
18+
}
19+
20+
/** @inheritdoc */
21+
validateTransaction(transaction?: FungibleAssetTransaction): void {
22+
if (!transaction) {
23+
throw new Error('fungible asset transaction not defined');
24+
}
25+
super.validateTransaction(transaction);
26+
this.validateAddress({ address: transaction.assetId });
27+
}
28+
29+
protected isValidTransactionPayload(payload: TransactionPayload) {
30+
try {
31+
if (
32+
!(payload instanceof TransactionPayloadEntryFunction) ||
33+
payload.entryFunction.args.length !== 3 ||
34+
payload.entryFunction.type_args.length !== 1 ||
35+
FUNGIBLE_ASSET !== payload.entryFunction.type_args[0].toString()
36+
) {
37+
console.error('invalid transaction payload');
38+
return false;
39+
}
40+
const entryFunction = payload.entryFunction;
41+
const fungibleTokenAddress = entryFunction.args[0].toString();
42+
const recipientAddress = entryFunction.args[1].toString();
43+
const amountBuffer = Buffer.from(entryFunction.args[2].bcsToBytes());
44+
const recipientAmount = new BigNumber(amountBuffer.readBigUint64LE().toString());
45+
return (
46+
utils.isValidAddress(recipientAddress) &&
47+
utils.isValidAddress(fungibleTokenAddress) &&
48+
!recipientAmount.isLessThan(0)
49+
);
50+
} catch (e) {
51+
console.error('invalid transaction payload', e);
52+
return false;
53+
}
54+
}
55+
}

0 commit comments

Comments
 (0)