Skip to content

Commit 6efbd37

Browse files
committed
feat(sdk-coin-apt): sendMany multiple recipients native aptos
TICKET: COIN-3558
1 parent 58d2811 commit 6efbd37

File tree

7 files changed

+296
-33
lines changed

7 files changed

+296
-33
lines changed

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@ export const APT_SIGNATURE_LENGTH = 128;
55
export const UNAVAILABLE_TEXT = 'UNAVAILABLE';
66
export const DEFAULT_GAS_UNIT_PRICE = 100;
77
export const SECONDS_PER_WEEK = 7 * 24 * 60 * 60; // Days * Hours * Minutes * Seconds
8+
export const ADDRESS_BYTES_LENGTH = 32;
9+
export const AMOUNT_BYTES_LENGTH = 8;
810

911
export const DIGITAL_ASSET_TRANSFER_AMOUNT = '1';
1012

1113
export const FUNGIBLE_ASSET_TRANSFER_FUNCTION = '0x1::primary_fungible_store::transfer';
1214
export const COIN_TRANSFER_FUNCTION = '0x1::aptos_account::transfer_coins';
15+
export const COIN_BATCH_TRANSFER_FUNCTION = '0x1::aptos_account::batch_transfer_coins';
1316
export const DIGITAL_ASSET_TRANSFER_FUNCTION = '0x1::object::transfer';
1417

1518
export const APTOS_COIN = '0x1::aptos_coin::AptosCoin';

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,11 @@ export interface TxData {
2626
feePayer: string;
2727
assetId: string;
2828
}
29+
30+
export interface RecipientsValidationResult {
31+
recipients: {
32+
deserializedAddresses: string[];
33+
deserializedAmounts: Uint8Array[];
34+
};
35+
isValid: boolean;
36+
}

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

Lines changed: 49 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@ import {
44
AccountAddress,
55
Aptos,
66
AptosConfig,
7+
EntryFunctionArgument,
8+
InputGenerateTransactionPayloadData,
79
Network,
810
TransactionPayload,
911
TransactionPayloadEntryFunction,
1012
} from '@aptos-labs/ts-sdk';
1113

1214
import { BaseCoin as CoinConfig, NetworkType } from '@bitgo/statics';
13-
import { APTOS_COIN, COIN_TRANSFER_FUNCTION } from '../constants';
15+
import { APTOS_COIN, COIN_BATCH_TRANSFER_FUNCTION, COIN_TRANSFER_FUNCTION } from '../constants';
1416
import utils from '../utils';
1517

1618
export class TransferTransaction extends Transaction {
@@ -21,36 +23,24 @@ export class TransferTransaction extends Transaction {
2123
}
2224

2325
protected parseTransactionPayload(payload: TransactionPayload): void {
24-
if (
25-
!(payload instanceof TransactionPayloadEntryFunction) ||
26-
payload.entryFunction.args.length !== 2 ||
27-
payload.entryFunction.type_args.length !== 1 ||
28-
payload.entryFunction.type_args[0].toString().length === 0
29-
) {
26+
if (!this.isValidPayload(payload)) {
3027
throw new InvalidTransactionError('Invalid transaction payload');
3128
}
32-
const entryFunction = payload.entryFunction;
29+
const entryFunction = (payload as TransactionPayloadEntryFunction).entryFunction;
3330
this._assetId = entryFunction.type_args[0].toString();
34-
this.recipients = [
35-
{
36-
address: entryFunction.args[0].toString(),
37-
amount: utils.getAmountFromPayloadArgs(entryFunction.args[1].bcsToBytes()),
38-
},
39-
] as TransactionRecipient[];
31+
const addressArg = entryFunction.args[0];
32+
const amountArg = entryFunction.args[1];
33+
this.recipients = this.parseRecipients(addressArg, amountArg);
4034
}
4135

4236
protected async buildRawTransaction(): Promise<void> {
4337
const network: Network = this._coinConfig.network.type === NetworkType.MAINNET ? Network.MAINNET : Network.TESTNET;
4438
const aptos = new Aptos(new AptosConfig({ network }));
4539
const senderAddress = AccountAddress.fromString(this._sender);
46-
const recipientAddress = AccountAddress.fromString(this.recipients[0].address);
40+
4741
const simpleTxn = await aptos.transaction.build.simple({
4842
sender: senderAddress,
49-
data: {
50-
function: COIN_TRANSFER_FUNCTION,
51-
typeArguments: [this.assetId],
52-
functionArguments: [recipientAddress, this.recipients[0].amount],
53-
},
43+
data: this.buildData(this.recipients) as InputGenerateTransactionPayloadData,
5444
options: {
5545
maxGasAmount: this.maxGasAmount,
5646
gasUnitPrice: this.gasUnitPrice,
@@ -60,4 +50,43 @@ export class TransferTransaction extends Transaction {
6050
});
6151
this._rawTransaction = simpleTxn.rawTransaction;
6252
}
53+
54+
private isValidPayload(payload: TransactionPayload): boolean {
55+
return (
56+
payload instanceof TransactionPayloadEntryFunction &&
57+
payload.entryFunction.args.length === 2 &&
58+
payload.entryFunction.type_args.length === 1 &&
59+
payload.entryFunction.type_args[0].toString().length > 0
60+
);
61+
}
62+
63+
private parseRecipients(addressArg: EntryFunctionArgument, amountArg: EntryFunctionArgument): TransactionRecipient[] {
64+
const { recipients, isValid } = utils.fetchAndValidateRecipients(addressArg, amountArg);
65+
if (!isValid) {
66+
throw new InvalidTransactionError('Invalid transaction recipients');
67+
}
68+
return recipients.deserializedAddresses.map((address, index) => ({
69+
address,
70+
amount: utils.getAmountFromPayloadArgs(recipients.deserializedAmounts[index]),
71+
})) as TransactionRecipient[];
72+
}
73+
74+
private buildData(recipients: TransactionRecipient[]): InputGenerateTransactionPayloadData {
75+
if (recipients.length > 1) {
76+
return {
77+
function: COIN_BATCH_TRANSFER_FUNCTION,
78+
typeArguments: [this.assetId],
79+
functionArguments: [
80+
recipients.map((recipient) => AccountAddress.fromString(recipient.address)),
81+
recipients.map((recipient) => recipient.amount),
82+
],
83+
};
84+
} else {
85+
return {
86+
function: COIN_TRANSFER_FUNCTION,
87+
typeArguments: [this.assetId],
88+
functionArguments: [AccountAddress.fromString(recipients[0].address), recipients[0].amount],
89+
};
90+
}
91+
}
6392
}

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

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { TransactionBuilder } from './transactionBuilder';
22
import { BaseCoin as CoinConfig } from '@bitgo/statics';
33
import { TransactionType } from '@bitgo/sdk-core';
44
import { TransferTransaction } from '../transaction/transferTransaction';
5-
import BigNumber from 'bignumber.js';
65
import utils from '../utils';
76
import { TransactionPayload, TransactionPayloadEntryFunction } from '@aptos-labs/ts-sdk';
87

@@ -29,26 +28,27 @@ export class TransferBuilder extends TransactionBuilder {
2928
return this;
3029
}
3130

32-
protected isValidTransactionPayload(payload: TransactionPayload) {
31+
protected isValidTransactionPayload(payload: TransactionPayload): boolean {
3332
try {
34-
if (
35-
!(payload instanceof TransactionPayloadEntryFunction) ||
36-
payload.entryFunction.args.length !== 2 ||
37-
payload.entryFunction.type_args.length !== 1 ||
38-
payload.entryFunction.type_args[0].toString().length === 0
39-
) {
33+
if (!this.isValidPayload(payload)) {
4034
console.error('invalid transaction payload');
4135
return false;
4236
}
43-
const entryFunction = payload.entryFunction;
37+
const entryFunction = (payload as TransactionPayloadEntryFunction).entryFunction;
4438
this.validatePartsOfAssetId(entryFunction.type_args[0].toString());
45-
const recipientAddress = entryFunction.args[0].toString();
46-
const amountBuffer = Buffer.from(entryFunction.args[1].bcsToBytes());
47-
const recipientAmount = new BigNumber(amountBuffer.readBigUint64LE().toString());
48-
return utils.isValidAddress(recipientAddress) && !recipientAmount.isLessThan(0);
39+
return utils.fetchAndValidateRecipients(entryFunction.args[0], entryFunction.args[1]).isValid;
4940
} catch (e) {
5041
console.error('invalid transaction payload', e);
5142
return false;
5243
}
5344
}
45+
46+
private isValidPayload(payload: TransactionPayload): boolean {
47+
return (
48+
payload instanceof TransactionPayloadEntryFunction &&
49+
payload.entryFunction.args.length === 2 &&
50+
payload.entryFunction.type_args.length === 1 &&
51+
payload.entryFunction.type_args[0].toString().length > 0
52+
);
53+
}
5454
}

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

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import {
2+
AccountAddress,
23
AuthenticationKey,
34
Deserializer,
45
Ed25519PublicKey,
6+
EntryFunctionArgument,
57
Hex,
68
SignedTransaction,
79
TransactionPayload,
810
TransactionPayloadEntryFunction,
11+
U64,
912
} from '@aptos-labs/ts-sdk';
1013
import {
1114
BaseUtils,
@@ -19,12 +22,16 @@ import {
1922
APT_BLOCK_ID_LENGTH,
2023
APT_SIGNATURE_LENGTH,
2124
APT_TRANSACTION_ID_LENGTH,
25+
COIN_BATCH_TRANSFER_FUNCTION,
2226
COIN_TRANSFER_FUNCTION,
2327
DIGITAL_ASSET_TRANSFER_FUNCTION,
2428
FUNGIBLE_ASSET_TRANSFER_FUNCTION,
2529
SECONDS_PER_WEEK,
30+
ADDRESS_BYTES_LENGTH,
31+
AMOUNT_BYTES_LENGTH,
2632
} from './constants';
2733
import BigNumber from 'bignumber.js';
34+
import { RecipientsValidationResult } from './iface';
2835

2936
export class Utils implements BaseUtils {
3037
/** @inheritdoc */
@@ -80,6 +87,7 @@ export class Utils implements BaseUtils {
8087
const uniqueIdentifier = `${moduleAddress}::${moduleIdentifier}::${functionIdentifier}`;
8188
switch (uniqueIdentifier) {
8289
case COIN_TRANSFER_FUNCTION:
90+
case COIN_BATCH_TRANSFER_FUNCTION:
8391
return TransactionType.Send;
8492
case FUNGIBLE_ASSET_TRANSFER_FUNCTION:
8593
return TransactionType.SendToken;
@@ -90,12 +98,53 @@ export class Utils implements BaseUtils {
9098
}
9199
}
92100

101+
fetchAndValidateRecipients(
102+
addressArg: EntryFunctionArgument,
103+
amountArg: EntryFunctionArgument
104+
): RecipientsValidationResult {
105+
const addressBytes = addressArg.bcsToBytes();
106+
const amountBytes = amountArg.bcsToBytes();
107+
let deserializedAddresses: string[];
108+
let deserializedAmounts: Uint8Array<ArrayBuffer>[];
109+
if (addressBytes.length > ADDRESS_BYTES_LENGTH || amountBytes.length > AMOUNT_BYTES_LENGTH) {
110+
deserializedAddresses = utils.deserializeAccountAddressVector(addressBytes);
111+
deserializedAmounts = utils.deserializeU64Vector(amountBytes);
112+
if (deserializedAddresses.length !== deserializedAmounts.length) {
113+
console.error('invalid payload entry function arguments : addresses and amounts length mismatch');
114+
return { recipients: { deserializedAddresses, deserializedAmounts }, isValid: false };
115+
}
116+
} else {
117+
deserializedAddresses = [addressArg.toString()];
118+
deserializedAmounts = [amountBytes];
119+
}
120+
const allAddressesValid = deserializedAddresses.every((address) => utils.isValidAddress(address.toString()));
121+
const allAmountsValid = deserializedAmounts.every((amount) =>
122+
new BigNumber(utils.getAmountFromPayloadArgs(amount)).isGreaterThan(0)
123+
);
124+
return {
125+
recipients: { deserializedAddresses, deserializedAmounts },
126+
isValid: allAddressesValid && allAmountsValid,
127+
};
128+
}
129+
93130
deserializeSignedTransaction(rawTransaction: string): SignedTransaction {
94131
const txnBytes = Hex.fromHexString(rawTransaction).toUint8Array();
95132
const deserializer = new Deserializer(txnBytes);
96133
return deserializer.deserialize(SignedTransaction);
97134
}
98135

136+
deserializeAccountAddressVector(serializedBytes: Uint8Array): string[] {
137+
const deserializer = new Deserializer(serializedBytes);
138+
const deserializedAddresses = deserializer.deserializeVector(AccountAddress);
139+
return deserializedAddresses.map((address) => address.toString());
140+
}
141+
142+
deserializeU64Vector(serializedBytes: Uint8Array): Uint8Array[] {
143+
const deserializer = new Deserializer(serializedBytes);
144+
const deserializedAmounts = deserializer.deserializeVector(U64);
145+
return deserializedAmounts.map((amount) => amount.bcsToBytes());
146+
}
147+
99148
getBufferFromHexString(hexString: string): Buffer {
100149
return Buffer.from(Hex.fromHexString(hexString).toUint8Array());
101150
}

modules/sdk-coin-apt/test/resources/apt.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,17 @@ export const recipients: Recipient[] = [
4343
},
4444
];
4545

46+
export const batchRecipients: Recipient[] = [
47+
{
48+
address: '0xdd52c0b72a73696b867d6571a308c413e43bff8f44956a5991abc4d50db0b849',
49+
amount: '1000',
50+
},
51+
{
52+
address: '0x2a81760d52db9a96df2609860218214b6d1012e77e84a3fed5145a9a65bf6932',
53+
amount: '1000',
54+
},
55+
];
56+
4657
export const fungibleTokenRecipients: Recipient[] = [
4758
{
4859
address: addresses.validAddresses[0],
@@ -88,6 +99,9 @@ export const FUNGIBLE_TOKEN_TRANSFER =
8899
export const DIGITAL_ASSET_TRANSFER =
89100
'0x1aed808916ab9b1b30b07abb53561afd46847285ce28651221d406173a372449ab00000000000000020000000000000000000000000000000000000000000000000000000000000001066f626a656374087472616e736665720107000000000000000000000000000000000000000000000000000000000000000405746f6b656e05546f6b656e0002202e356062777469d39ca5d9b72512ce2d5713d7938ed6ca9193d4fc2016a819fd20f7405c28a02cf5bab4ea4498240bb3579db45951794eb1c843bef0534c093ad9400d0300000000006400000000000000526798670000000002030020f73836f42257240e43d439552471fc9dbcc3f1af5bd0b4ed83f44b5f66146442400c2c31eae495d22d1d31031b9756e8238a3df6e760515aa3e1108d93b3aec6aeb91684307e8365ad9dc44c0b5957e95a21a6f47d8d4a6e0eb3b145fb3d517f030000dbc87a1c816d9bcd06b683c37e80c7162e4d48da7812198b830e4d5d8e0629f200205223396c531f13e031a9f0cb26d459d799a52e51be9a1cb9e871afb4c31f91ff40dd6c064e44642819dec2f63d32d6daa4f889d62d06025ad99b42562c4c6cdef8b1437739115d7f38050078829efb9dd05528f53309bcab5b89fadb283423100d';
90101

102+
export const COIN_BATCH_TRANSFER =
103+
'0xc8f02d25aa698b3e9fbd8a08e8da4c8ee261832a25a4cde8731b5ec356537d0951000000000000000200000000000000000000000000000000000000000000000000000000000000010d6170746f735f6163636f756e741462617463685f7472616e736665725f636f696e73010700000000000000000000000000000000000000000000000000000000000000010a6170746f735f636f696e094170746f73436f696e00024102dd52c0b72a73696b867d6571a308c413e43bff8f44956a5991abc4d50db0b8492a81760d52db9a96df2609860218214b6d1012e77e84a3fed5145a9a65bf69321102e803000000000000e803000000000000400d030000000000640000000000000009d4ea6700000000020300202121dcd098069ae535697dd019cfd8677ca7aba0adac1d1959cbce6dc54b12594047602d08426ce035971e86d57d57fc33a7e9d29da56a87a2e6e9d12ae3b39e25892298fa8b080111c609c9a451264b320236a06a45eab870765f90ffc4a42a030000dbc87a1c816d9bcd06b683c37e80c7162e4d48da7812198b830e4d5d8e0629f200205223396c531f13e031a9f0cb26d459d799a52e51be9a1cb9e871afb4c31f91ff401d817a641b915727057babb0ddfe6876583ac5c8d4b86b4c91385dc94f071f8e12a4908786a417b2721fa9838dd1bee6e153ca02f78de43161a9810407ea0b0f';
104+
91105
export const INVALID_TRANSFER =
92106
'AAAAAAAAAAAAA6e7361637469bc4a58e500b9e64cb6547ee9b403000000000000002064ba1fb2f2fbd2938a350015d601f4db89cd7e8e2370d0dd9ae3ac4f635c1581111b8a49f67370bc4a58e500b9e64cb6462e39b802000000000000002064ba1fb2f2fbd2938a350015d601f4db89cd7e8e2370d0dd9ae3ac47aa1ff81f01c4173a804406a365e69dfb297d4eaaf002546ebd016400000000000000cba4a48bb0f8b586c167e5dcefaa1c5e96ab3f0836d6ca08f2081732944d1e5b6b406a4a462e39b8030000000000000020b9490ede63215262c434e03f606d9799f3ba704523ceda184b386d47aa1ff81f01000000000000006400000000000000';
93107

0 commit comments

Comments
 (0)