Skip to content

Commit 2f73951

Browse files
committed
feat(sdk-coin-apt): staking delegate instruction
Ticket: SC-3599 TICKET: SC-3599
1 parent 2828138 commit 2f73951

File tree

9 files changed

+312
-2
lines changed

9 files changed

+312
-2
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export const FUNGIBLE_ASSET_BATCH_TRANSFER_FUNCTION = '0x1::aptos_account::batch
1515
export const COIN_TRANSFER_FUNCTION = '0x1::aptos_account::transfer_coins';
1616
export const COIN_BATCH_TRANSFER_FUNCTION = '0x1::aptos_account::batch_transfer_coins';
1717
export const DIGITAL_ASSET_TRANSFER_FUNCTION = '0x1::object::transfer';
18+
export const DELEGATION_POOL_ADD_STAKE_FUNCTION = '0x1::delegation_pool::add_stake';
1819

1920
export const APTOS_COIN = '0x1::aptos_coin::AptosCoin';
2021
export const FUNGIBLE_ASSET_TYPE_ARGUMENT = '0x1::fungible_asset::Metadata';
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { Transaction } from './transaction';
2+
import { InvalidTransactionError, TransactionType } from '@bitgo/sdk-core';
3+
import {
4+
AccountAddress,
5+
EntryFunctionABI,
6+
InputGenerateTransactionPayloadData,
7+
TransactionPayload,
8+
TransactionPayloadEntryFunction,
9+
TypeTagAddress,
10+
TypeTagU64,
11+
} from '@aptos-labs/ts-sdk';
12+
13+
import { BaseCoin as CoinConfig } from '@bitgo/statics';
14+
import { APTOS_COIN, DELEGATION_POOL_ADD_STAKE_FUNCTION } from '../constants';
15+
import utils from '../utils';
16+
17+
export class DelegationPoolAddStakeTransaction extends Transaction {
18+
constructor(coinConfig: Readonly<CoinConfig>) {
19+
super(coinConfig);
20+
this._type = TransactionType.StakingDelegate;
21+
this._assetId = APTOS_COIN;
22+
}
23+
24+
protected parseTransactionPayload(payload: TransactionPayload): void {
25+
if (!this.isValidPayload(payload)) {
26+
throw new InvalidTransactionError('Invalid transaction payload');
27+
}
28+
const { entryFunction } = payload;
29+
const addressArg = entryFunction.args[0];
30+
const amountArg = entryFunction.args[1];
31+
this.recipients = utils.parseRecipients(addressArg, amountArg);
32+
}
33+
34+
protected getTransactionPayloadData(): InputGenerateTransactionPayloadData {
35+
return {
36+
function: DELEGATION_POOL_ADD_STAKE_FUNCTION,
37+
typeArguments: [],
38+
functionArguments: [AccountAddress.fromString(this.recipients[0].address), this.recipients[0].amount],
39+
abi: this.abi,
40+
};
41+
}
42+
43+
private isValidPayload(payload: TransactionPayload): payload is TransactionPayloadEntryFunction {
44+
return (
45+
payload instanceof TransactionPayloadEntryFunction &&
46+
payload.entryFunction.args.length === 2 &&
47+
payload.entryFunction.type_args.length === 0
48+
);
49+
}
50+
51+
private abi: EntryFunctionABI = {
52+
typeParameters: [],
53+
parameters: [new TypeTagAddress(), new TypeTagU64()],
54+
};
55+
}

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,6 @@ export abstract class Transaction extends BaseTransaction {
304304
try {
305305
signedTxn = utils.deserializeSignedTransaction(rawTransaction);
306306
} catch (e) {
307-
console.error('invalid raw transaction', e);
308307
throw new Error('invalid raw transaction');
309308
}
310309
this.fromDeserializedSignedTransaction(signedTxn);
@@ -318,7 +317,6 @@ export abstract class Transaction extends BaseTransaction {
318317
try {
319318
return utils.deserializeSignedTransaction(signedRawTransaction);
320319
} catch (e) {
321-
console.error('invalid raw transaction', e);
322320
throw new Error('invalid raw transaction');
323321
}
324322
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { TransactionBuilder } from './transactionBuilder';
2+
import { BaseCoin as CoinConfig } from '@bitgo/statics';
3+
import { TransactionType } from '@bitgo/sdk-core';
4+
import utils from '../utils';
5+
import { TransactionPayload, TransactionPayloadEntryFunction } from '@aptos-labs/ts-sdk';
6+
import { DelegationPoolAddStakeTransaction } from '../transaction/delegationPoolAddStakeTransaction';
7+
8+
export class DelegationPoolAddStakeTransactionBuilder extends TransactionBuilder {
9+
constructor(_coinConfig: Readonly<CoinConfig>) {
10+
super(_coinConfig);
11+
this.transaction = new DelegationPoolAddStakeTransaction(_coinConfig);
12+
}
13+
14+
protected get transactionType(): TransactionType {
15+
return TransactionType.StakingDelegate;
16+
}
17+
18+
assetId(_assetId: string): TransactionBuilder {
19+
this.transaction.assetId = _assetId;
20+
return this;
21+
}
22+
23+
protected isValidTransactionPayload(payload: TransactionPayload): boolean {
24+
try {
25+
if (!this.isValidPayload(payload)) {
26+
return false;
27+
}
28+
const { entryFunction } = payload;
29+
const addressArg = entryFunction.args[0];
30+
const amountArg = entryFunction.args[1];
31+
return utils.fetchAndValidateRecipients(addressArg, amountArg).isValid;
32+
} catch (e) {
33+
return false;
34+
}
35+
}
36+
37+
private isValidPayload(payload: TransactionPayload): payload is TransactionPayloadEntryFunction {
38+
return (
39+
payload instanceof TransactionPayloadEntryFunction &&
40+
payload.entryFunction.args.length === 2 &&
41+
payload.entryFunction.type_args.length === 0
42+
);
43+
}
44+
}

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import { DigitalAssetTransfer } from './transaction/digitalAssetTransfer';
1212
import { DigitalAssetTransferBuilder } from './transactionBuilder/digitalAssetTransferBuilder';
1313
import { CustomTransaction } from './transaction/customTransaction';
1414
import { CustomTransactionBuilder } from './transactionBuilder/customTransactionBuilder';
15+
import { DelegationPoolAddStakeTransaction } from './transaction/delegationPoolAddStakeTransaction';
16+
import { DelegationPoolAddStakeTransactionBuilder } from './transactionBuilder/delegationPoolAddStakeTransactionBuilder';
1517

1618
export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
1719
constructor(_coinConfig: Readonly<CoinConfig>) {
@@ -37,6 +39,10 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
3739
const digitalAssetTransferTx = new DigitalAssetTransfer(this._coinConfig);
3840
digitalAssetTransferTx.fromDeserializedSignedTransaction(signedTxn);
3941
return this.getDigitalAssetTransactionBuilder(digitalAssetTransferTx);
42+
case TransactionType.StakingDelegate:
43+
const delegateTx = new DelegationPoolAddStakeTransaction(this._coinConfig);
44+
delegateTx.fromDeserializedSignedTransaction(signedTxn);
45+
return this.getDelegationPoolAddStakeTransactionBuilder(delegateTx);
4046
case TransactionType.CustomTx:
4147
const customTx = new CustomTransaction(this._coinConfig);
4248
if (abi) {
@@ -72,6 +78,10 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
7278
return this.initializeBuilder(tx, new DigitalAssetTransferBuilder(this._coinConfig));
7379
}
7480

81+
getDelegationPoolAddStakeTransactionBuilder(tx?: Transaction): DelegationPoolAddStakeTransactionBuilder {
82+
return this.initializeBuilder(tx, new DelegationPoolAddStakeTransactionBuilder(this._coinConfig));
83+
}
84+
7585
/**
7686
* Get a custom transaction builder
7787
*

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
APT_TRANSACTION_ID_LENGTH,
2929
COIN_BATCH_TRANSFER_FUNCTION,
3030
COIN_TRANSFER_FUNCTION,
31+
DELEGATION_POOL_ADD_STAKE_FUNCTION,
3132
DIGITAL_ASSET_TRANSFER_FUNCTION,
3233
FUNGIBLE_ASSET_BATCH_TRANSFER_FUNCTION,
3334
FUNGIBLE_ASSET_TRANSFER_FUNCTION,
@@ -97,6 +98,8 @@ export class Utils implements BaseUtils {
9798
return TransactionType.SendToken;
9899
case DIGITAL_ASSET_TRANSFER_FUNCTION:
99100
return TransactionType.SendNFT;
101+
case DELEGATION_POOL_ADD_STAKE_FUNCTION:
102+
return TransactionType.StakingDelegate;
100103
default:
101104
// For any other function calls, treat as a custom transaction
102105
return TransactionType.CustomTx;

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,13 @@ export const digitalTokenRecipients: Recipient[] = [
8383
},
8484
];
8585

86+
export const delegationPoolAddStakeRecipients: Recipient[] = [
87+
{
88+
address: addresses.validAddresses[0],
89+
amount: AMOUNT.toString(),
90+
},
91+
];
92+
8693
export const invalidRecipients: Recipient[] = [
8794
{
8895
address: addresses.invalidAddresses[0],
@@ -135,3 +142,9 @@ export const FUNGIBLE_BATCH_RAW_TX_HEX =
135142

136143
export const FUNGIBLE_BATCH_SIGNABLE_PAYLOAD =
137144
'5efa3c4f02f83a0f4b2d69fc95c607cc02825cc4e7be536ef0992df050d9e67c011aed808916ab9b1b30b07abb53561afd46847285ce28651221d406173a3724490e000000000000000200000000000000000000000000000000000000000000000000000000000000010d6170746f735f6163636f756e741e62617463685f7472616e736665725f66756e6769626c655f617373657473000320d5d0d561493ea2b9410f67da804653ae44e793c2423707d4f11edb2e381920504102dd52c0b72a73696b867d6571a308c413e43bff8f44956a5991abc4d50db0b8492a81760d52db9a96df2609860218214b6d1012e77e84a3fed5145a9a65bf6932110201000000000000000100000000000000400d03000000000064000000000000008b037d67000000000200dbc87a1c816d9bcd06b683c37e80c7162e4d48da7812198b830e4d5d8e0629f2';
145+
146+
export const DELEGATION_POOL_ADD_STAKE_TX_HEX =
147+
'0x1aed808916ab9b1b30b07abb53561afd46847285ce28651221d406173a3724490e000000000000000200000000000000000000000000000000000000000000000000000000000000010f64656c65676174696f6e5f706f6f6c096164645f7374616b65000220f7405c28a02cf5bab4ea4498240bb3579db45951794eb1c843bef0534c093ad908e803000000000000400d03000000000064000000000000008b037d670000000002030020000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dbc87a1c816d9bcd06b683c37e80c7162e4d48da7812198b830e4d5d8e0629f2002000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000';
148+
149+
export const DELEGATION_POOL_ADD_STAKE_TX_HEX_SIGNED =
150+
'5efa3c4f02f83a0f4b2d69fc95c607cc02825cc4e7be536ef0992df050d9e67c011aed808916ab9b1b30b07abb53561afd46847285ce28651221d406173a3724490e000000000000000200000000000000000000000000000000000000000000000000000000000000010f64656c65676174696f6e5f706f6f6c096164645f7374616b65000220f7405c28a02cf5bab4ea4498240bb3579db45951794eb1c843bef0534c093ad908e803000000000000400d03000000000064000000000000008b037d67000000000200dbc87a1c816d9bcd06b683c37e80c7162e4d48da7812198b830e4d5d8e0629f2';

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
} from '@aptos-labs/ts-sdk';
2020
import utils from '../../src/lib/utils';
2121
import { AptCoin, coins, GasTankAccountCoin } from '@bitgo/statics';
22+
import { DelegationPoolAddStakeTransaction } from 'modules/sdk-coin-apt/src/lib/transaction/delegationPoolAddStakeTransaction';
2223

2324
describe('APT:', function () {
2425
let bitgo: TestBitGoAPI;
@@ -229,6 +230,39 @@ describe('APT:', function () {
229230
});
230231
});
231232

233+
it('should explain a staking delegate transaction', async function () {
234+
const rawTx = testData.DELEGATION_POOL_ADD_STAKE_TX_HEX;
235+
const transaction = new DelegationPoolAddStakeTransaction(coins.get('tapt'));
236+
transaction.fromRawTransaction(rawTx);
237+
const explainedTx = transaction.explainTransaction();
238+
explainedTx.should.deepEqual({
239+
displayOrder: [
240+
'id',
241+
'outputs',
242+
'outputAmount',
243+
'changeOutputs',
244+
'changeAmount',
245+
'fee',
246+
'withdrawAmount',
247+
'sender',
248+
'type',
249+
],
250+
id: '0xc5b960d1bec149c77896344774352c61441307af564eaa8c84f857208e411bf3',
251+
outputs: [
252+
{
253+
address: '0xf7405c28a02cf5bab4ea4498240bb3579db45951794eb1c843bef0534c093ad9',
254+
amount: '1000',
255+
},
256+
],
257+
outputAmount: '1000',
258+
changeOutputs: [],
259+
changeAmount: '0',
260+
fee: { fee: '0' },
261+
sender: '0x1aed808916ab9b1b30b07abb53561afd46847285ce28651221d406173a372449',
262+
type: 30,
263+
});
264+
});
265+
232266
it('should fail to explain a invalid raw transaction', async function () {
233267
const rawTx = 'invalidRawTransaction';
234268
const transaction = new TransferTransaction(coins.get('tapt'));
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import { getBuilderFactory } from '../getBuilderFactory';
2+
import { coins } from '@bitgo/statics';
3+
import * as testData from '../../resources/apt';
4+
import { TransactionType } from '@bitgo/sdk-core';
5+
import should from 'should';
6+
import { DelegationPoolAddStakeTransaction } from 'modules/sdk-coin-apt/src/lib/transaction/delegationPoolAddStakeTransaction';
7+
8+
describe('Apt Token Transfer Builder', () => {
9+
const factory = getBuilderFactory('tapt');
10+
11+
describe('Succeed', () => {
12+
it('should build a staking delegate transaction', async function () {
13+
const transaction = new DelegationPoolAddStakeTransaction(coins.get('tapt'));
14+
const txBuilder = factory.getDelegationPoolAddStakeTransactionBuilder(transaction);
15+
txBuilder.sender(testData.sender.address);
16+
txBuilder.recipients(testData.delegationPoolAddStakeRecipients);
17+
txBuilder.gasData({
18+
maxGasAmount: 200000,
19+
gasUnitPrice: 100,
20+
});
21+
txBuilder.sequenceNumber(14);
22+
txBuilder.expirationTime(1736246155);
23+
txBuilder.addFeePayerAddress(testData.feePayer.address);
24+
const tx = (await txBuilder.build()) as DelegationPoolAddStakeTransaction;
25+
should.equal(tx.sender, testData.sender.address);
26+
should.equal(tx.recipients[0].address, testData.delegationPoolAddStakeRecipients[0].address);
27+
should.equal(tx.recipients[0].amount, testData.delegationPoolAddStakeRecipients[0].amount);
28+
should.equal(tx.maxGasAmount, 200000);
29+
should.equal(tx.gasUnitPrice, 100);
30+
should.equal(tx.sequenceNumber, 14);
31+
should.equal(tx.expirationTime, 1736246155);
32+
should.equal(tx.type, TransactionType.StakingDelegate);
33+
should.deepEqual(tx.inputs, [
34+
{
35+
address: testData.sender.address,
36+
value: testData.delegationPoolAddStakeRecipients[0].amount,
37+
coin: 'tapt',
38+
},
39+
]);
40+
should.deepEqual(tx.outputs, [
41+
{
42+
address: testData.delegationPoolAddStakeRecipients[0].address,
43+
value: testData.delegationPoolAddStakeRecipients[0].amount,
44+
coin: 'tapt',
45+
},
46+
]);
47+
const rawTx = tx.toBroadcastFormat();
48+
should.equal(txBuilder.isValidRawTransaction(rawTx), true);
49+
rawTx.should.equal(testData.DELEGATION_POOL_ADD_STAKE_TX_HEX);
50+
});
51+
52+
it('should build and send a signed tx', async function () {
53+
const txBuilder = factory.from(testData.DELEGATION_POOL_ADD_STAKE_TX_HEX);
54+
const tx = (await txBuilder.build()) as DelegationPoolAddStakeTransaction;
55+
tx.inputs.should.deepEqual([
56+
{
57+
address: testData.sender.address,
58+
value: testData.delegationPoolAddStakeRecipients[0].amount,
59+
coin: 'tapt',
60+
},
61+
]);
62+
tx.outputs.should.deepEqual([
63+
{
64+
address: testData.delegationPoolAddStakeRecipients[0].address,
65+
value: testData.delegationPoolAddStakeRecipients[0].amount,
66+
coin: 'tapt',
67+
},
68+
]);
69+
should.equal(tx.id, '0xc5b960d1bec149c77896344774352c61441307af564eaa8c84f857208e411bf3');
70+
should.equal(tx.maxGasAmount, 200000);
71+
should.equal(tx.gasUnitPrice, 100);
72+
should.equal(tx.sequenceNumber, 14);
73+
should.equal(tx.expirationTime, 1736246155);
74+
should.equal(tx.type, TransactionType.StakingDelegate);
75+
const rawTx = tx.toBroadcastFormat();
76+
should.equal(txBuilder.isValidRawTransaction(rawTx), true);
77+
should.equal(rawTx, testData.DELEGATION_POOL_ADD_STAKE_TX_HEX);
78+
});
79+
80+
it('should succeed to validate a valid signablePayload', async function () {
81+
const transaction = new DelegationPoolAddStakeTransaction(coins.get('tapt'));
82+
const txBuilder = factory.getDelegationPoolAddStakeTransactionBuilder(transaction);
83+
txBuilder.sender(testData.sender.address);
84+
txBuilder.recipients([testData.delegationPoolAddStakeRecipients[0]]);
85+
txBuilder.gasData({
86+
maxGasAmount: 200000,
87+
gasUnitPrice: 100,
88+
});
89+
txBuilder.sequenceNumber(14);
90+
txBuilder.expirationTime(1736246155);
91+
txBuilder.addFeePayerAddress(testData.feePayer.address);
92+
const tx = (await txBuilder.build()) as DelegationPoolAddStakeTransaction;
93+
const signablePayload = tx.signablePayload;
94+
should.equal(signablePayload.toString('hex'), testData.DELEGATION_POOL_ADD_STAKE_TX_HEX_SIGNED);
95+
});
96+
97+
it('should build a unsigned tx and validate its toJson', async function () {
98+
const transaction = new DelegationPoolAddStakeTransaction(coins.get('tapt'));
99+
const txBuilder = factory.getDelegationPoolAddStakeTransactionBuilder(transaction);
100+
txBuilder.sender(testData.sender.address);
101+
txBuilder.recipients([testData.delegationPoolAddStakeRecipients[0]]);
102+
txBuilder.gasData({
103+
maxGasAmount: 200000,
104+
gasUnitPrice: 100,
105+
});
106+
txBuilder.sequenceNumber(14);
107+
txBuilder.expirationTime(1736246155);
108+
txBuilder.assetId(testData.fungibleTokenAddress.usdt);
109+
txBuilder.addFeePayerAddress(testData.feePayer.address);
110+
const tx = (await txBuilder.build()) as DelegationPoolAddStakeTransaction;
111+
const toJson = tx.toJson();
112+
should.equal(toJson.sender, testData.sender.address);
113+
should.deepEqual(toJson.recipients, [
114+
{
115+
address: testData.delegationPoolAddStakeRecipients[0].address,
116+
amount: testData.delegationPoolAddStakeRecipients[0].amount,
117+
},
118+
]);
119+
should.deepEqual(toJson.recipient, {
120+
address: testData.delegationPoolAddStakeRecipients[0].address,
121+
amount: testData.delegationPoolAddStakeRecipients[0].amount,
122+
});
123+
should.equal(toJson.sequenceNumber, 14);
124+
should.equal(toJson.maxGasAmount, 200000);
125+
should.equal(toJson.gasUnitPrice, 100);
126+
should.equal(toJson.expirationTime, 1736246155);
127+
should.equal(toJson.feePayer, testData.feePayer.address);
128+
});
129+
130+
it('should build a signed tx and validate its toJson', async function () {
131+
const txBuilder = factory.from(testData.DELEGATION_POOL_ADD_STAKE_TX_HEX);
132+
const tx = (await txBuilder.build()) as DelegationPoolAddStakeTransaction;
133+
const toJson = tx.toJson();
134+
should.equal(toJson.id, '0xc5b960d1bec149c77896344774352c61441307af564eaa8c84f857208e411bf3');
135+
should.equal(toJson.sender, testData.sender.address);
136+
should.deepEqual(toJson.recipients, [
137+
{
138+
address: testData.delegationPoolAddStakeRecipients[0].address,
139+
amount: testData.delegationPoolAddStakeRecipients[0].amount.toString(),
140+
},
141+
]);
142+
should.deepEqual(toJson.recipient, {
143+
address: testData.delegationPoolAddStakeRecipients[0].address,
144+
amount: testData.delegationPoolAddStakeRecipients[0].amount.toString(),
145+
});
146+
should.equal(toJson.maxGasAmount, 200000);
147+
should.equal(toJson.gasUnitPrice, 100);
148+
should.equal(toJson.sequenceNumber, 14);
149+
should.equal(toJson.expirationTime, 1736246155);
150+
});
151+
});
152+
});

0 commit comments

Comments
 (0)