Skip to content

Commit daea860

Browse files
authored
Merge pull request #7304 from BitGo/harit/SC-3599-apt-add-stake
feat(sdk-coin-apt): staking delegate instruction
2 parents 49d9aa8 + 9e42ac3 commit daea860

File tree

10 files changed

+409
-2
lines changed

10 files changed

+409
-2
lines changed

examples/ts/apt/stake-apt.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/**
2+
* Performs delegated staking with Aptos.
3+
*
4+
* Copyright 2025, BitGo, Inc. All Rights Reserved.
5+
*/
6+
import { BitGoAPI } from '@bitgo/sdk-api';
7+
import { coins } from '@bitgo/statics';
8+
import { Tapt, TransactionBuilderFactory, Utils } from '@bitgo/sdk-coin-apt';
9+
import { Network, Aptos, AptosConfig, Account, Ed25519PrivateKey, SimpleTransaction } from '@aptos-labs/ts-sdk';
10+
11+
require('dotenv').config({ path: '../../.env' });
12+
13+
const AMOUNT_OCTAS = 11 * 100_000_000;
14+
const NETWORK = Network.TESTNET;
15+
16+
const aptosConfig = new AptosConfig({ network: NETWORK });
17+
const aptos = new Aptos(aptosConfig);
18+
19+
const bitgo = new BitGoAPI({
20+
accessToken: process.env.TESTNET_ACCESS_TOKEN,
21+
env: 'test',
22+
});
23+
const coin = coins.get('tapt');
24+
bitgo.register(coin.name, Tapt.createInstance);
25+
26+
const broadcastToSimple = (serializedTx: string) =>
27+
new SimpleTransaction(Utils.default.deserializeSignedTransaction(serializedTx).raw_txn);
28+
29+
async function main() {
30+
const account = getAccount();
31+
const delegationPoolAddress = getDelegationPoolAddress();
32+
33+
// Account should have sufficient balance
34+
const accountBalance = await aptos.getAccountAPTAmount({ accountAddress: account.accountAddress });
35+
if (accountBalance < AMOUNT_OCTAS) {
36+
console.info(`Balance of ${account.accountAddress} is ${accountBalance} octas, requesting funds.`);
37+
const txn = await aptos.fundAccount({ accountAddress: account.accountAddress, amount: AMOUNT_OCTAS });
38+
await aptos.waitForTransaction({ transactionHash: txn.hash });
39+
console.info(`Funding successful: ${txn.hash}`);
40+
}
41+
const { sequence_number } = await aptos.getAccountInfo({ accountAddress: account.accountAddress });
42+
43+
// Use BitGoAPI to build instruction
44+
const txBuilder = new TransactionBuilderFactory(coin).getDelegationPoolAddStakeTransactionBuilder();
45+
txBuilder
46+
.sender(account.accountAddress.toString())
47+
.recipients([{ address: delegationPoolAddress, amount: `${AMOUNT_OCTAS}` }])
48+
.sequenceNumber(Number(sequence_number));
49+
const unsignedTx = await txBuilder.build();
50+
const serializedUnsignedTx = unsignedTx.toBroadcastFormat();
51+
52+
// Sign transaction. Signing is flexible, let's use Aptos libs
53+
const authenticator = aptos.sign({
54+
signer: account,
55+
transaction: broadcastToSimple(serializedUnsignedTx),
56+
});
57+
if (!authenticator.isEd25519()) {
58+
throw new Error('Example only supports Ed25519');
59+
}
60+
txBuilder.addSenderSignature(
61+
{ pub: account.publicKey.toString() },
62+
Buffer.from(authenticator.signature.toUint8Array())
63+
);
64+
const tx = await txBuilder.build();
65+
const serializedTx = tx.toBroadcastFormat();
66+
console.info(`Transaction ${serializedTx} and JSON:\n${JSON.stringify(tx.toJson(), undefined, 2)}`);
67+
68+
// Submit transaction
69+
const submittedTxn = await aptos.transaction.submit.simple({
70+
transaction: broadcastToSimple(serializedTx),
71+
senderAuthenticator: authenticator,
72+
});
73+
console.log(`Success: ${submittedTxn.hash}`);
74+
}
75+
76+
const getAccount = () => {
77+
const privateKey = process.env.APTOS_PRIVATE_KEY;
78+
if (privateKey === undefined) {
79+
const { privateKey } = Account.generate();
80+
console.log('# Here is a new account to save into your .env file.');
81+
console.log(`APTOS_PRIVATE_KEY=${privateKey.toAIP80String()}`);
82+
throw new Error('Missing account information');
83+
}
84+
return Account.fromPrivateKey({ privateKey: new Ed25519PrivateKey(privateKey) });
85+
};
86+
87+
const getDelegationPoolAddress = () => {
88+
const address = process.env.APTOS_DELEGATION_POOL_ADDRESS;
89+
if (!address) {
90+
console.log('# Provide a delegation pool.');
91+
console.log(`APTOS_DELEGATION_POOL_ADDRESS=`);
92+
throw new Error('Missing delegation pool address');
93+
}
94+
return address;
95+
};
96+
97+
main().catch((e) => console.error(e));

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_SIGNABLE_PAYLOAD =
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 '../../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'));

0 commit comments

Comments
 (0)