Skip to content

Commit 3bdeb41

Browse files
committed
feat(sdk-coin-apt): add delegation pool withdraw transaction
Ticket: SC-3601
1 parent e2cd6f4 commit 3bdeb41

File tree

7 files changed

+253
-0
lines changed

7 files changed

+253
-0
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export const COIN_BATCH_TRANSFER_FUNCTION = '0x1::aptos_account::batch_transfer_
1717
export const DIGITAL_ASSET_TRANSFER_FUNCTION = '0x1::object::transfer';
1818
export const DELEGATION_POOL_ADD_STAKE_FUNCTION = '0x1::delegation_pool::add_stake';
1919
export const DELEGATION_POOL_UNLOCK_FUNCTION = '0x1::delegation_pool::unlock';
20+
export const DELEGATION_POOL_WITHDRAW_FUNCTION = '0x1::delegation_pool::withdraw';
2021

2122
export const APTOS_COIN = '0x1::aptos_coin::AptosCoin';
2223
export const FUNGIBLE_ASSET_TYPE_ARGUMENT = '0x1::fungible_asset::Metadata';
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { MoveFunctionId } from '@aptos-labs/ts-sdk';
2+
import { TransactionType } from '@bitgo/sdk-core';
3+
4+
import { BaseCoin as CoinConfig } from '@bitgo/statics';
5+
import { DELEGATION_POOL_WITHDRAW_FUNCTION } from '../constants';
6+
import { AbstractDelegationPoolAmountBasedTransaction } from './abstractDelegationPoolAmountBasedTransaction';
7+
import { InputsAndOutputs } from './transaction';
8+
9+
export class DelegationPoolWithdrawTransaction extends AbstractDelegationPoolAmountBasedTransaction {
10+
constructor(coinConfig: Readonly<CoinConfig>) {
11+
super(coinConfig);
12+
this._type = TransactionType.StakingWithdraw;
13+
}
14+
15+
override moveFunctionId(): MoveFunctionId {
16+
return DELEGATION_POOL_WITHDRAW_FUNCTION;
17+
}
18+
19+
override inputsAndOutputs(): InputsAndOutputs {
20+
const { sender, validatorAddress, amount } = this;
21+
if (sender === undefined) throw new Error('sender is undefined');
22+
if (validatorAddress === undefined) throw new Error('validatorAddress is undefined');
23+
if (amount === undefined) throw new Error('amount is undefined');
24+
return {
25+
inputs: [
26+
{
27+
address: validatorAddress,
28+
value: amount,
29+
coin: this._coinConfig.name,
30+
},
31+
],
32+
outputs: [
33+
{
34+
address: sender,
35+
value: amount,
36+
coin: this._coinConfig.name,
37+
},
38+
],
39+
externalOutputs: [],
40+
};
41+
}
42+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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 { DelegationPoolWithdrawTransaction } from '../transaction/delegationPoolWithdrawTransaction';
7+
8+
export class DelegationPoolWithdrawTransactionBuilder extends TransactionBuilder {
9+
protected override _transaction: DelegationPoolWithdrawTransaction;
10+
11+
constructor(_coinConfig: Readonly<CoinConfig>) {
12+
super(_coinConfig);
13+
this.transaction = new DelegationPoolWithdrawTransaction(_coinConfig);
14+
}
15+
16+
protected get transactionType(): TransactionType {
17+
return TransactionType.StakingWithdraw;
18+
}
19+
20+
assetId(_assetId: string): TransactionBuilder {
21+
this.transaction.assetId = _assetId;
22+
return this;
23+
}
24+
25+
validator(validatorAddress: string, amount: string): TransactionBuilder {
26+
this._transaction.validatorAddress = validatorAddress;
27+
this._transaction.amount = amount;
28+
return this;
29+
}
30+
31+
protected isValidTransactionPayload(payload: TransactionPayload): boolean {
32+
try {
33+
if (!this.isValidPayload(payload)) {
34+
return false;
35+
}
36+
const { entryFunction } = payload;
37+
const addressArg = entryFunction.args[0];
38+
const amountArg = entryFunction.args[1];
39+
return utils.fetchAndValidateRecipients(addressArg, amountArg).isValid;
40+
} catch (e) {
41+
return false;
42+
}
43+
}
44+
45+
private isValidPayload(payload: TransactionPayload): payload is TransactionPayloadEntryFunction {
46+
return (
47+
payload instanceof TransactionPayloadEntryFunction &&
48+
payload.entryFunction.args.length === 2 &&
49+
payload.entryFunction.type_args.length === 0
50+
);
51+
}
52+
}

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ import { CustomTransactionBuilder } from './transactionBuilder/customTransaction
1515
import { DelegationPoolAddStakeTransaction } from './transaction/delegationPoolAddStakeTransaction';
1616
import { DelegationPoolAddStakeTransactionBuilder } from './transactionBuilder/delegationPoolAddStakeTransactionBuilder';
1717
import { DelegationPoolUnlockTransaction } from './transaction/delegationPoolUnlockTransaction';
18+
import { DelegationPoolWithdrawTransactionBuilder } from './transactionBuilder/delegationPoolWithdrawTransactionBuilder';
1819
import { DelegationPoolUnlockTransactionBuilder } from './transactionBuilder/delegationPoolUnlockTransactionBuilder';
20+
import { DelegationPoolWithdrawTransaction } from './transaction/delegationPoolWithdrawTransaction';
1921

2022
export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
2123
constructor(_coinConfig: Readonly<CoinConfig>) {
@@ -49,6 +51,10 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
4951
const unlockTx = new DelegationPoolUnlockTransaction(this._coinConfig);
5052
unlockTx.fromDeserializedSignedTransaction(signedTxn);
5153
return this.getDelegationPoolUnlockTransactionBuilder(unlockTx);
54+
case TransactionType.StakingWithdraw:
55+
const withdrawTx = new DelegationPoolWithdrawTransaction(this._coinConfig);
56+
withdrawTx.fromDeserializedSignedTransaction(signedTxn);
57+
return this.getDelegationPoolWithdrawTransactionBuilder(withdrawTx);
5258
case TransactionType.CustomTx:
5359
const customTx = new CustomTransaction(this._coinConfig);
5460
if (abi) {
@@ -92,6 +98,10 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
9298
return this.initializeBuilder(tx, new DelegationPoolUnlockTransactionBuilder(this._coinConfig));
9399
}
94100

101+
getDelegationPoolWithdrawTransactionBuilder(tx?: Transaction): DelegationPoolWithdrawTransactionBuilder {
102+
return this.initializeBuilder(tx, new DelegationPoolWithdrawTransactionBuilder(this._coinConfig));
103+
}
104+
95105
/**
96106
* Get a custom transaction builder
97107
*

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
COIN_TRANSFER_FUNCTION,
3131
DELEGATION_POOL_ADD_STAKE_FUNCTION,
3232
DELEGATION_POOL_UNLOCK_FUNCTION,
33+
DELEGATION_POOL_WITHDRAW_FUNCTION,
3334
DIGITAL_ASSET_TRANSFER_FUNCTION,
3435
FUNGIBLE_ASSET_BATCH_TRANSFER_FUNCTION,
3536
FUNGIBLE_ASSET_TRANSFER_FUNCTION,
@@ -103,6 +104,8 @@ export class Utils implements BaseUtils {
103104
return TransactionType.StakingDelegate;
104105
case DELEGATION_POOL_UNLOCK_FUNCTION:
105106
return TransactionType.StakingUnlock;
107+
case DELEGATION_POOL_WITHDRAW_FUNCTION:
108+
return TransactionType.StakingWithdraw;
106109
default:
107110
// For any other function calls, treat as a custom transaction
108111
return TransactionType.CustomTx;

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,8 +147,14 @@ export const DELEGATION_POOL_ADD_STAKE_TX_HEX =
147147
export const DELEGATION_POOL_UNLOCK_TX_HEX =
148148
'0x1aed808916ab9b1b30b07abb53561afd46847285ce28651221d406173a3724490e000000000000000200000000000000000000000000000000000000000000000000000000000000010f64656c65676174696f6e5f706f6f6c06756e6c6f636b000220f7405c28a02cf5bab4ea4498240bb3579db45951794eb1c843bef0534c093ad908e803000000000000400d03000000000064000000000000008b037d670000000002030020000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dbc87a1c816d9bcd06b683c37e80c7162e4d48da7812198b830e4d5d8e0629f2002000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000';
149149

150+
export const DELEGATION_POOL_WITHDRAW_TX_HEX =
151+
'0x1aed808916ab9b1b30b07abb53561afd46847285ce28651221d406173a3724490e000000000000000200000000000000000000000000000000000000000000000000000000000000010f64656c65676174696f6e5f706f6f6c087769746864726177000220f7405c28a02cf5bab4ea4498240bb3579db45951794eb1c843bef0534c093ad908e803000000000000400d03000000000064000000000000008b037d670000000002030020000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dbc87a1c816d9bcd06b683c37e80c7162e4d48da7812198b830e4d5d8e0629f2002000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000';
152+
150153
export const DELEGATION_POOL_ADD_STAKE_TX_HEX_SIGNABLE_PAYLOAD =
151154
'5efa3c4f02f83a0f4b2d69fc95c607cc02825cc4e7be536ef0992df050d9e67c011aed808916ab9b1b30b07abb53561afd46847285ce28651221d406173a3724490e000000000000000200000000000000000000000000000000000000000000000000000000000000010f64656c65676174696f6e5f706f6f6c096164645f7374616b65000220f7405c28a02cf5bab4ea4498240bb3579db45951794eb1c843bef0534c093ad908e803000000000000400d03000000000064000000000000008b037d67000000000200dbc87a1c816d9bcd06b683c37e80c7162e4d48da7812198b830e4d5d8e0629f2';
152155

153156
export const DELEGATION_POOL_UNLOCK_TX_HEX_SIGNABLE_PAYLOAD =
154157
'5efa3c4f02f83a0f4b2d69fc95c607cc02825cc4e7be536ef0992df050d9e67c011aed808916ab9b1b30b07abb53561afd46847285ce28651221d406173a3724490e000000000000000200000000000000000000000000000000000000000000000000000000000000010f64656c65676174696f6e5f706f6f6c06756e6c6f636b000220f7405c28a02cf5bab4ea4498240bb3579db45951794eb1c843bef0534c093ad908e803000000000000400d03000000000064000000000000008b037d67000000000200dbc87a1c816d9bcd06b683c37e80c7162e4d48da7812198b830e4d5d8e0629f2';
158+
159+
export const DELEGATION_POOL_WITHDRAW_TX_HEX_SIGNABLE_PAYLOAD =
160+
'5efa3c4f02f83a0f4b2d69fc95c607cc02825cc4e7be536ef0992df050d9e67c011aed808916ab9b1b30b07abb53561afd46847285ce28651221d406173a3724490e000000000000000200000000000000000000000000000000000000000000000000000000000000010f64656c65676174696f6e5f706f6f6c087769746864726177000220f7405c28a02cf5bab4ea4498240bb3579db45951794eb1c843bef0534c093ad908e803000000000000400d03000000000064000000000000008b037d67000000000200dbc87a1c816d9bcd06b683c37e80c7162e4d48da7812198b830e4d5d8e0629f2';
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
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 { DelegationPoolWithdrawTransaction } from '../../../src/lib/transaction/delegationPoolWithdrawTransaction';
7+
8+
describe('Apt Delegation Pool Withdraw Builder', () => {
9+
const factory = getBuilderFactory('tapt');
10+
11+
describe('Succeed', () => {
12+
it('should build a staking withdraw transaction', async function () {
13+
const transaction = new DelegationPoolWithdrawTransaction(coins.get('tapt'));
14+
const txBuilder = factory.getDelegationPoolWithdrawTransactionBuilder(transaction);
15+
txBuilder.sender(testData.sender.address);
16+
txBuilder.validator(testData.delegationPoolData.validatorAddress, testData.delegationPoolData.amount);
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 DelegationPoolWithdrawTransaction;
25+
should.equal(tx.sender, testData.sender.address);
26+
should.deepEqual(tx.recipients, []);
27+
should.equal(tx.validatorAddress, testData.delegationPoolData.validatorAddress);
28+
should.equal(tx.amount, testData.delegationPoolData.amount);
29+
should.equal(tx.maxGasAmount, 200000);
30+
should.equal(tx.gasUnitPrice, 100);
31+
should.equal(tx.sequenceNumber, 14);
32+
should.equal(tx.expirationTime, 1736246155);
33+
should.equal(tx.type, TransactionType.StakingWithdraw);
34+
should.deepEqual(tx.inputs, [
35+
{
36+
address: testData.delegationPoolData.validatorAddress,
37+
value: testData.delegationPoolData.amount,
38+
coin: 'tapt',
39+
},
40+
]);
41+
should.deepEqual(tx.outputs, [
42+
{
43+
address: testData.sender.address,
44+
value: testData.delegationPoolData.amount,
45+
coin: 'tapt',
46+
},
47+
]);
48+
const rawTx = tx.toBroadcastFormat();
49+
should.equal(txBuilder.isValidRawTransaction(rawTx), true);
50+
rawTx.should.equal(testData.DELEGATION_POOL_WITHDRAW_TX_HEX);
51+
});
52+
53+
it('should build and send a signed tx', async function () {
54+
const txBuilder = factory.from(testData.DELEGATION_POOL_WITHDRAW_TX_HEX);
55+
const tx = (await txBuilder.build()) as DelegationPoolWithdrawTransaction;
56+
tx.inputs.should.deepEqual([
57+
{
58+
address: testData.delegationPoolData.validatorAddress,
59+
value: testData.delegationPoolData.amount,
60+
coin: 'tapt',
61+
},
62+
]);
63+
tx.outputs.should.deepEqual([
64+
{
65+
address: testData.sender.address,
66+
value: testData.delegationPoolData.amount,
67+
coin: 'tapt',
68+
},
69+
]);
70+
should.equal(tx.id, '0xd795391e85ffd5e37b844db4206c5bd99ba28a42430df996969ee9b7f16a5f21');
71+
should.equal(tx.maxGasAmount, 200000);
72+
should.equal(tx.gasUnitPrice, 100);
73+
should.equal(tx.sequenceNumber, 14);
74+
should.equal(tx.expirationTime, 1736246155);
75+
should.equal(tx.type, TransactionType.StakingWithdraw);
76+
const rawTx = tx.toBroadcastFormat();
77+
should.equal(txBuilder.isValidRawTransaction(rawTx), true);
78+
should.equal(rawTx, testData.DELEGATION_POOL_WITHDRAW_TX_HEX);
79+
});
80+
81+
it('should succeed to validate a valid signablePayload', async function () {
82+
const transaction = new DelegationPoolWithdrawTransaction(coins.get('tapt'));
83+
const txBuilder = factory.getDelegationPoolWithdrawTransactionBuilder(transaction);
84+
txBuilder.sender(testData.sender.address);
85+
txBuilder.validator(testData.delegationPoolData.validatorAddress, testData.delegationPoolData.amount);
86+
txBuilder.gasData({
87+
maxGasAmount: 200000,
88+
gasUnitPrice: 100,
89+
});
90+
txBuilder.sequenceNumber(14);
91+
txBuilder.expirationTime(1736246155);
92+
txBuilder.addFeePayerAddress(testData.feePayer.address);
93+
const tx = (await txBuilder.build()) as DelegationPoolWithdrawTransaction;
94+
const signablePayload = tx.signablePayload;
95+
should.equal(signablePayload.toString('hex'), testData.DELEGATION_POOL_WITHDRAW_TX_HEX_SIGNABLE_PAYLOAD);
96+
});
97+
98+
it('should build a unsigned tx and validate its toJson', async function () {
99+
const transaction = new DelegationPoolWithdrawTransaction(coins.get('tapt'));
100+
const txBuilder = factory.getDelegationPoolWithdrawTransactionBuilder(transaction);
101+
txBuilder.sender(testData.sender.address);
102+
txBuilder.validator(testData.delegationPoolData.validatorAddress, testData.delegationPoolData.amount);
103+
txBuilder.gasData({
104+
maxGasAmount: 200000,
105+
gasUnitPrice: 100,
106+
});
107+
txBuilder.sequenceNumber(14);
108+
txBuilder.expirationTime(1736246155);
109+
txBuilder.assetId(testData.fungibleTokenAddress.usdt);
110+
txBuilder.addFeePayerAddress(testData.feePayer.address);
111+
const tx = (await txBuilder.build()) as DelegationPoolWithdrawTransaction;
112+
const toJson = tx.toJson();
113+
should.equal(toJson.sender, testData.sender.address);
114+
should.deepEqual(toJson.recipients, []);
115+
should.equal(toJson.validatorAddress, testData.delegationPoolData.validatorAddress);
116+
should.equal(toJson.amount, testData.delegationPoolData.amount);
117+
should.equal(toJson.sequenceNumber, 14);
118+
should.equal(toJson.maxGasAmount, 200000);
119+
should.equal(toJson.gasUnitPrice, 100);
120+
should.equal(toJson.expirationTime, 1736246155);
121+
should.equal(toJson.feePayer, testData.feePayer.address);
122+
});
123+
124+
it('should build a signed tx and validate its toJson', async function () {
125+
const txBuilder = factory.from(testData.DELEGATION_POOL_WITHDRAW_TX_HEX);
126+
const tx = (await txBuilder.build()) as DelegationPoolWithdrawTransaction;
127+
const toJson = tx.toJson();
128+
should.equal(toJson.id, '0xd795391e85ffd5e37b844db4206c5bd99ba28a42430df996969ee9b7f16a5f21');
129+
should.equal(toJson.sender, testData.sender.address);
130+
should.deepEqual(toJson.recipients, []);
131+
should.equal(toJson.validatorAddress, testData.delegationPoolData.validatorAddress);
132+
should.equal(toJson.amount, testData.delegationPoolData.amount);
133+
should.equal(toJson.maxGasAmount, 200000);
134+
should.equal(toJson.gasUnitPrice, 100);
135+
should.equal(toJson.sequenceNumber, 14);
136+
should.equal(toJson.expirationTime, 1736246155);
137+
});
138+
});
139+
});

0 commit comments

Comments
 (0)