Skip to content

Commit e2cd6f4

Browse files
committed
feat(sdk-coin-apt): add delegation pool unlock transaction
Ticket: SC-3600
1 parent 3b76e15 commit e2cd6f4

File tree

7 files changed

+213
-0
lines changed

7 files changed

+213
-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
@@ -16,6 +16,7 @@ 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';
1818
export const DELEGATION_POOL_ADD_STAKE_FUNCTION = '0x1::delegation_pool::add_stake';
19+
export const DELEGATION_POOL_UNLOCK_FUNCTION = '0x1::delegation_pool::unlock';
1920

2021
export const APTOS_COIN = '0x1::aptos_coin::AptosCoin';
2122
export const FUNGIBLE_ASSET_TYPE_ARGUMENT = '0x1::fungible_asset::Metadata';
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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_UNLOCK_FUNCTION } from '../constants';
6+
import { AbstractDelegationPoolAmountBasedTransaction } from './abstractDelegationPoolAmountBasedTransaction';
7+
import { InputsAndOutputs } from './transaction';
8+
9+
export class DelegationPoolUnlockTransaction extends AbstractDelegationPoolAmountBasedTransaction {
10+
constructor(coinConfig: Readonly<CoinConfig>) {
11+
super(coinConfig);
12+
this._type = TransactionType.StakingUnlock;
13+
}
14+
15+
override moveFunctionId(): MoveFunctionId {
16+
return DELEGATION_POOL_UNLOCK_FUNCTION;
17+
}
18+
19+
override inputsAndOutputs(): InputsAndOutputs {
20+
return {
21+
inputs: [],
22+
outputs: [],
23+
externalOutputs: [],
24+
};
25+
}
26+
}
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 { DelegationPoolUnlockTransaction } from '../transaction/delegationPoolUnlockTransaction';
7+
8+
export class DelegationPoolUnlockTransactionBuilder extends TransactionBuilder {
9+
protected override _transaction: DelegationPoolUnlockTransaction;
10+
11+
constructor(_coinConfig: Readonly<CoinConfig>) {
12+
super(_coinConfig);
13+
this.transaction = new DelegationPoolUnlockTransaction(_coinConfig);
14+
}
15+
16+
protected get transactionType(): TransactionType {
17+
return TransactionType.StakingUnlock;
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
@@ -14,6 +14,8 @@ import { CustomTransaction } from './transaction/customTransaction';
1414
import { CustomTransactionBuilder } from './transactionBuilder/customTransactionBuilder';
1515
import { DelegationPoolAddStakeTransaction } from './transaction/delegationPoolAddStakeTransaction';
1616
import { DelegationPoolAddStakeTransactionBuilder } from './transactionBuilder/delegationPoolAddStakeTransactionBuilder';
17+
import { DelegationPoolUnlockTransaction } from './transaction/delegationPoolUnlockTransaction';
18+
import { DelegationPoolUnlockTransactionBuilder } from './transactionBuilder/delegationPoolUnlockTransactionBuilder';
1719

1820
export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
1921
constructor(_coinConfig: Readonly<CoinConfig>) {
@@ -43,6 +45,10 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
4345
const delegateTx = new DelegationPoolAddStakeTransaction(this._coinConfig);
4446
delegateTx.fromDeserializedSignedTransaction(signedTxn);
4547
return this.getDelegationPoolAddStakeTransactionBuilder(delegateTx);
48+
case TransactionType.StakingUnlock:
49+
const unlockTx = new DelegationPoolUnlockTransaction(this._coinConfig);
50+
unlockTx.fromDeserializedSignedTransaction(signedTxn);
51+
return this.getDelegationPoolUnlockTransactionBuilder(unlockTx);
4652
case TransactionType.CustomTx:
4753
const customTx = new CustomTransaction(this._coinConfig);
4854
if (abi) {
@@ -82,6 +88,10 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
8288
return this.initializeBuilder(tx, new DelegationPoolAddStakeTransactionBuilder(this._coinConfig));
8389
}
8490

91+
getDelegationPoolUnlockTransactionBuilder(tx?: Transaction): DelegationPoolUnlockTransactionBuilder {
92+
return this.initializeBuilder(tx, new DelegationPoolUnlockTransactionBuilder(this._coinConfig));
93+
}
94+
8595
/**
8696
* Get a custom transaction builder
8797
*

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
COIN_BATCH_TRANSFER_FUNCTION,
3030
COIN_TRANSFER_FUNCTION,
3131
DELEGATION_POOL_ADD_STAKE_FUNCTION,
32+
DELEGATION_POOL_UNLOCK_FUNCTION,
3233
DIGITAL_ASSET_TRANSFER_FUNCTION,
3334
FUNGIBLE_ASSET_BATCH_TRANSFER_FUNCTION,
3435
FUNGIBLE_ASSET_TRANSFER_FUNCTION,
@@ -100,6 +101,8 @@ export class Utils implements BaseUtils {
100101
return TransactionType.SendNFT;
101102
case DELEGATION_POOL_ADD_STAKE_FUNCTION:
102103
return TransactionType.StakingDelegate;
104+
case DELEGATION_POOL_UNLOCK_FUNCTION:
105+
return TransactionType.StakingUnlock;
103106
default:
104107
// For any other function calls, treat as a custom transaction
105108
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
@@ -144,5 +144,11 @@ export const FUNGIBLE_BATCH_SIGNABLE_PAYLOAD =
144144
export const DELEGATION_POOL_ADD_STAKE_TX_HEX =
145145
'0x1aed808916ab9b1b30b07abb53561afd46847285ce28651221d406173a3724490e000000000000000200000000000000000000000000000000000000000000000000000000000000010f64656c65676174696f6e5f706f6f6c096164645f7374616b65000220f7405c28a02cf5bab4ea4498240bb3579db45951794eb1c843bef0534c093ad908e803000000000000400d03000000000064000000000000008b037d670000000002030020000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dbc87a1c816d9bcd06b683c37e80c7162e4d48da7812198b830e4d5d8e0629f2002000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000';
146146

147+
export const DELEGATION_POOL_UNLOCK_TX_HEX =
148+
'0x1aed808916ab9b1b30b07abb53561afd46847285ce28651221d406173a3724490e000000000000000200000000000000000000000000000000000000000000000000000000000000010f64656c65676174696f6e5f706f6f6c06756e6c6f636b000220f7405c28a02cf5bab4ea4498240bb3579db45951794eb1c843bef0534c093ad908e803000000000000400d03000000000064000000000000008b037d670000000002030020000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dbc87a1c816d9bcd06b683c37e80c7162e4d48da7812198b830e4d5d8e0629f2002000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000';
149+
147150
export const DELEGATION_POOL_ADD_STAKE_TX_HEX_SIGNABLE_PAYLOAD =
148151
'5efa3c4f02f83a0f4b2d69fc95c607cc02825cc4e7be536ef0992df050d9e67c011aed808916ab9b1b30b07abb53561afd46847285ce28651221d406173a3724490e000000000000000200000000000000000000000000000000000000000000000000000000000000010f64656c65676174696f6e5f706f6f6c096164645f7374616b65000220f7405c28a02cf5bab4ea4498240bb3579db45951794eb1c843bef0534c093ad908e803000000000000400d03000000000064000000000000008b037d67000000000200dbc87a1c816d9bcd06b683c37e80c7162e4d48da7812198b830e4d5d8e0629f2';
152+
153+
export const DELEGATION_POOL_UNLOCK_TX_HEX_SIGNABLE_PAYLOAD =
154+
'5efa3c4f02f83a0f4b2d69fc95c607cc02825cc4e7be536ef0992df050d9e67c011aed808916ab9b1b30b07abb53561afd46847285ce28651221d406173a3724490e000000000000000200000000000000000000000000000000000000000000000000000000000000010f64656c65676174696f6e5f706f6f6c06756e6c6f636b000220f7405c28a02cf5bab4ea4498240bb3579db45951794eb1c843bef0534c093ad908e803000000000000400d03000000000064000000000000008b037d67000000000200dbc87a1c816d9bcd06b683c37e80c7162e4d48da7812198b830e4d5d8e0629f2';
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
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 { DelegationPoolUnlockTransaction } from '../../../src/lib/transaction/delegationPoolUnlockTransaction';
7+
8+
describe('Apt Delegation Pool Unlock Builder', () => {
9+
const factory = getBuilderFactory('tapt');
10+
11+
describe('Succeed', () => {
12+
it('should build a staking delegate transaction', async function () {
13+
const transaction = new DelegationPoolUnlockTransaction(coins.get('tapt'));
14+
const txBuilder = factory.getDelegationPoolUnlockTransactionBuilder(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 DelegationPoolUnlockTransaction;
25+
should.equal(tx.sender, testData.sender.address);
26+
should.deepEqual(tx.recipients, []);
27+
should.deepEqual(tx.validatorAddress, testData.delegationPoolData.validatorAddress);
28+
should.deepEqual(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.StakingUnlock);
34+
should.deepEqual(tx.inputs, []);
35+
should.deepEqual(tx.outputs, []);
36+
const rawTx = tx.toBroadcastFormat();
37+
should.equal(txBuilder.isValidRawTransaction(rawTx), true);
38+
rawTx.should.equal(testData.DELEGATION_POOL_UNLOCK_TX_HEX);
39+
});
40+
41+
it('should build and send a signed tx', async function () {
42+
const txBuilder = factory.from(testData.DELEGATION_POOL_UNLOCK_TX_HEX);
43+
const tx = (await txBuilder.build()) as DelegationPoolUnlockTransaction;
44+
tx.inputs.should.deepEqual([]);
45+
tx.outputs.should.deepEqual([]);
46+
should.equal(tx.id, '0x471bb32955f9cff7c9c0a603ef2354e781fd80a221f9044f08df84d95473e86f');
47+
should.equal(tx.maxGasAmount, 200000);
48+
should.equal(tx.gasUnitPrice, 100);
49+
should.equal(tx.sequenceNumber, 14);
50+
should.equal(tx.expirationTime, 1736246155);
51+
should.equal(tx.type, TransactionType.StakingUnlock);
52+
const rawTx = tx.toBroadcastFormat();
53+
should.equal(txBuilder.isValidRawTransaction(rawTx), true);
54+
should.equal(rawTx, testData.DELEGATION_POOL_UNLOCK_TX_HEX);
55+
});
56+
57+
it('should succeed to validate a valid signablePayload', async function () {
58+
const transaction = new DelegationPoolUnlockTransaction(coins.get('tapt'));
59+
const txBuilder = factory.getDelegationPoolUnlockTransactionBuilder(transaction);
60+
txBuilder.sender(testData.sender.address);
61+
txBuilder.validator(testData.delegationPoolData.validatorAddress, testData.delegationPoolData.amount);
62+
txBuilder.gasData({
63+
maxGasAmount: 200000,
64+
gasUnitPrice: 100,
65+
});
66+
txBuilder.sequenceNumber(14);
67+
txBuilder.expirationTime(1736246155);
68+
txBuilder.addFeePayerAddress(testData.feePayer.address);
69+
const tx = (await txBuilder.build()) as DelegationPoolUnlockTransaction;
70+
const signablePayload = tx.signablePayload;
71+
should.equal(signablePayload.toString('hex'), testData.DELEGATION_POOL_UNLOCK_TX_HEX_SIGNABLE_PAYLOAD);
72+
});
73+
74+
it('should build a unsigned tx and validate its toJson', async function () {
75+
const transaction = new DelegationPoolUnlockTransaction(coins.get('tapt'));
76+
const txBuilder = factory.getDelegationPoolUnlockTransactionBuilder(transaction);
77+
txBuilder.sender(testData.sender.address);
78+
txBuilder.validator(testData.delegationPoolData.validatorAddress, testData.delegationPoolData.amount);
79+
txBuilder.gasData({
80+
maxGasAmount: 200000,
81+
gasUnitPrice: 100,
82+
});
83+
txBuilder.sequenceNumber(14);
84+
txBuilder.expirationTime(1736246155);
85+
txBuilder.assetId(testData.fungibleTokenAddress.usdt);
86+
txBuilder.addFeePayerAddress(testData.feePayer.address);
87+
const tx = (await txBuilder.build()) as DelegationPoolUnlockTransaction;
88+
const toJson = tx.toJson();
89+
should.equal(toJson.sender, testData.sender.address);
90+
should.deepEqual(toJson.recipients, []);
91+
should.deepEqual(tx.validatorAddress, testData.delegationPoolData.validatorAddress);
92+
should.deepEqual(tx.amount, testData.delegationPoolData.amount);
93+
should.equal(toJson.sequenceNumber, 14);
94+
should.equal(toJson.maxGasAmount, 200000);
95+
should.equal(toJson.gasUnitPrice, 100);
96+
should.equal(toJson.expirationTime, 1736246155);
97+
should.equal(toJson.feePayer, testData.feePayer.address);
98+
});
99+
100+
it('should build a signed tx and validate its toJson', async function () {
101+
const txBuilder = factory.from(testData.DELEGATION_POOL_UNLOCK_TX_HEX);
102+
const tx = (await txBuilder.build()) as DelegationPoolUnlockTransaction;
103+
const toJson = tx.toJson();
104+
should.equal(toJson.id, '0x471bb32955f9cff7c9c0a603ef2354e781fd80a221f9044f08df84d95473e86f');
105+
should.equal(toJson.sender, testData.sender.address);
106+
should.deepEqual(toJson.recipients, []);
107+
should.deepEqual(tx.validatorAddress, testData.delegationPoolData.validatorAddress);
108+
should.deepEqual(tx.amount, testData.delegationPoolData.amount);
109+
should.equal(toJson.maxGasAmount, 200000);
110+
should.equal(toJson.gasUnitPrice, 100);
111+
should.equal(toJson.sequenceNumber, 14);
112+
should.equal(toJson.expirationTime, 1736246155);
113+
});
114+
});
115+
});

0 commit comments

Comments
 (0)