Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions examples/ts/apt/stake-apt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/**
* Performs delegated staking with Aptos.
*
* Copyright 2025, BitGo, Inc. All Rights Reserved.
*/
import { BitGoAPI } from '@bitgo/sdk-api';
import { coins } from '@bitgo/statics';
import { Tapt, TransactionBuilderFactory, Utils } from '@bitgo/sdk-coin-apt';
import { Network, Aptos, AptosConfig, Account, Ed25519PrivateKey, SimpleTransaction } from '@aptos-labs/ts-sdk';

require('dotenv').config({ path: '../../.env' });

const AMOUNT_OCTAS = 11 * 100_000_000;
const NETWORK = Network.TESTNET;

const aptosConfig = new AptosConfig({ network: NETWORK });
const aptos = new Aptos(aptosConfig);

const bitgo = new BitGoAPI({
accessToken: process.env.TESTNET_ACCESS_TOKEN,
env: 'test',
});
const coin = coins.get('tapt');
bitgo.register(coin.name, Tapt.createInstance);

const broadcastToSimple = (serializedTx: string) =>
new SimpleTransaction(Utils.default.deserializeSignedTransaction(serializedTx).raw_txn);

async function main() {
const account = getAccount();
const delegationPoolAddress = getDelegationPoolAddress();

// Account should have sufficient balance
const accountBalance = await aptos.getAccountAPTAmount({ accountAddress: account.accountAddress });
if (accountBalance < AMOUNT_OCTAS) {
console.info(`Balance of ${account.accountAddress} is ${accountBalance} octas, requesting funds.`);
const txn = await aptos.fundAccount({ accountAddress: account.accountAddress, amount: AMOUNT_OCTAS });
await aptos.waitForTransaction({ transactionHash: txn.hash });
console.info(`Funding successful: ${txn.hash}`);
}
const { sequence_number } = await aptos.getAccountInfo({ accountAddress: account.accountAddress });

// Use BitGoAPI to build instruction
const txBuilder = new TransactionBuilderFactory(coin).getDelegationPoolAddStakeTransactionBuilder();
txBuilder
.sender(account.accountAddress.toString())
.recipients([{ address: delegationPoolAddress, amount: `${AMOUNT_OCTAS}` }])
.sequenceNumber(Number(sequence_number));
const unsignedTx = await txBuilder.build();
const serializedUnsignedTx = unsignedTx.toBroadcastFormat();

// Sign transaction. Signing is flexible, let's use Aptos libs
const authenticator = aptos.sign({
signer: account,
transaction: broadcastToSimple(serializedUnsignedTx),
});
if (!authenticator.isEd25519()) {
throw new Error('Example only supports Ed25519');
}
txBuilder.addSenderSignature(
{ pub: account.publicKey.toString() },
Buffer.from(authenticator.signature.toUint8Array())
);
const tx = await txBuilder.build();
const serializedTx = tx.toBroadcastFormat();
console.info(`Transaction ${serializedTx} and JSON:\n${JSON.stringify(tx.toJson(), undefined, 2)}`);

// Submit transaction
const submittedTxn = await aptos.transaction.submit.simple({
transaction: broadcastToSimple(serializedTx),
senderAuthenticator: authenticator,
});
console.log(`Success: ${submittedTxn.hash}`);
}

const getAccount = () => {
const privateKey = process.env.APTOS_PRIVATE_KEY;
if (privateKey === undefined) {
const { privateKey } = Account.generate();
console.log('# Here is a new account to save into your .env file.');
console.log(`APTOS_PRIVATE_KEY=${privateKey.toAIP80String()}`);
throw new Error('Missing account information');
}
return Account.fromPrivateKey({ privateKey: new Ed25519PrivateKey(privateKey) });
};

const getDelegationPoolAddress = () => {
const address = process.env.APTOS_DELEGATION_POOL_ADDRESS;
if (!address) {
console.log('# Provide a delegation pool.');
console.log(`APTOS_DELEGATION_POOL_ADDRESS=`);
throw new Error('Missing delegation pool address');
}
return address;
};

main().catch((e) => console.error(e));
1 change: 1 addition & 0 deletions modules/sdk-coin-apt/src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const FUNGIBLE_ASSET_BATCH_TRANSFER_FUNCTION = '0x1::aptos_account::batch
export const COIN_TRANSFER_FUNCTION = '0x1::aptos_account::transfer_coins';
export const COIN_BATCH_TRANSFER_FUNCTION = '0x1::aptos_account::batch_transfer_coins';
export const DIGITAL_ASSET_TRANSFER_FUNCTION = '0x1::object::transfer';
export const DELEGATION_POOL_ADD_STAKE_FUNCTION = '0x1::delegation_pool::add_stake';

export const APTOS_COIN = '0x1::aptos_coin::AptosCoin';
export const FUNGIBLE_ASSET_TYPE_ARGUMENT = '0x1::fungible_asset::Metadata';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Transaction } from './transaction';
import { InvalidTransactionError, TransactionType } from '@bitgo/sdk-core';
import {
AccountAddress,
EntryFunctionABI,
InputGenerateTransactionPayloadData,
TransactionPayload,
TransactionPayloadEntryFunction,
TypeTagAddress,
TypeTagU64,
} from '@aptos-labs/ts-sdk';

import { BaseCoin as CoinConfig } from '@bitgo/statics';
import { APTOS_COIN, DELEGATION_POOL_ADD_STAKE_FUNCTION } from '../constants';
import utils from '../utils';

export class DelegationPoolAddStakeTransaction extends Transaction {
constructor(coinConfig: Readonly<CoinConfig>) {
super(coinConfig);
this._type = TransactionType.StakingDelegate;
this._assetId = APTOS_COIN;
}

protected parseTransactionPayload(payload: TransactionPayload): void {
if (!this.isValidPayload(payload)) {
throw new InvalidTransactionError('Invalid transaction payload');
}
const { entryFunction } = payload;
const addressArg = entryFunction.args[0];
const amountArg = entryFunction.args[1];
this.recipients = utils.parseRecipients(addressArg, amountArg);
}

protected getTransactionPayloadData(): InputGenerateTransactionPayloadData {
return {
function: DELEGATION_POOL_ADD_STAKE_FUNCTION,
typeArguments: [],
functionArguments: [AccountAddress.fromString(this.recipients[0].address), this.recipients[0].amount],
abi: this.abi,
};
}

private isValidPayload(payload: TransactionPayload): payload is TransactionPayloadEntryFunction {
return (
payload instanceof TransactionPayloadEntryFunction &&
payload.entryFunction.args.length === 2 &&
payload.entryFunction.type_args.length === 0
);
}

private abi: EntryFunctionABI = {
typeParameters: [],
parameters: [new TypeTagAddress(), new TypeTagU64()],
};
}
2 changes: 0 additions & 2 deletions modules/sdk-coin-apt/src/lib/transaction/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,6 @@ export abstract class Transaction extends BaseTransaction {
try {
signedTxn = utils.deserializeSignedTransaction(rawTransaction);
} catch (e) {
console.error('invalid raw transaction', e);
throw new Error('invalid raw transaction');
}
this.fromDeserializedSignedTransaction(signedTxn);
Expand All @@ -318,7 +317,6 @@ export abstract class Transaction extends BaseTransaction {
try {
return utils.deserializeSignedTransaction(signedRawTransaction);
} catch (e) {
console.error('invalid raw transaction', e);
throw new Error('invalid raw transaction');
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { TransactionBuilder } from './transactionBuilder';
import { BaseCoin as CoinConfig } from '@bitgo/statics';
import { TransactionType } from '@bitgo/sdk-core';
import utils from '../utils';
import { TransactionPayload, TransactionPayloadEntryFunction } from '@aptos-labs/ts-sdk';
import { DelegationPoolAddStakeTransaction } from '../transaction/delegationPoolAddStakeTransaction';

export class DelegationPoolAddStakeTransactionBuilder extends TransactionBuilder {
constructor(_coinConfig: Readonly<CoinConfig>) {
super(_coinConfig);
this.transaction = new DelegationPoolAddStakeTransaction(_coinConfig);
}

protected get transactionType(): TransactionType {
return TransactionType.StakingDelegate;
}

assetId(_assetId: string): TransactionBuilder {
this.transaction.assetId = _assetId;
return this;
}

protected isValidTransactionPayload(payload: TransactionPayload): boolean {
try {
if (!this.isValidPayload(payload)) {
return false;
}
const { entryFunction } = payload;
const addressArg = entryFunction.args[0];
const amountArg = entryFunction.args[1];
return utils.fetchAndValidateRecipients(addressArg, amountArg).isValid;
} catch (e) {
return false;
}
}

private isValidPayload(payload: TransactionPayload): payload is TransactionPayloadEntryFunction {
return (
payload instanceof TransactionPayloadEntryFunction &&
payload.entryFunction.args.length === 2 &&
payload.entryFunction.type_args.length === 0
);
}
}
10 changes: 10 additions & 0 deletions modules/sdk-coin-apt/src/lib/transactionBuilderFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { DigitalAssetTransfer } from './transaction/digitalAssetTransfer';
import { DigitalAssetTransferBuilder } from './transactionBuilder/digitalAssetTransferBuilder';
import { CustomTransaction } from './transaction/customTransaction';
import { CustomTransactionBuilder } from './transactionBuilder/customTransactionBuilder';
import { DelegationPoolAddStakeTransaction } from './transaction/delegationPoolAddStakeTransaction';
import { DelegationPoolAddStakeTransactionBuilder } from './transactionBuilder/delegationPoolAddStakeTransactionBuilder';

export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
constructor(_coinConfig: Readonly<CoinConfig>) {
Expand All @@ -37,6 +39,10 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
const digitalAssetTransferTx = new DigitalAssetTransfer(this._coinConfig);
digitalAssetTransferTx.fromDeserializedSignedTransaction(signedTxn);
return this.getDigitalAssetTransactionBuilder(digitalAssetTransferTx);
case TransactionType.StakingDelegate:
const delegateTx = new DelegationPoolAddStakeTransaction(this._coinConfig);
delegateTx.fromDeserializedSignedTransaction(signedTxn);
return this.getDelegationPoolAddStakeTransactionBuilder(delegateTx);
case TransactionType.CustomTx:
const customTx = new CustomTransaction(this._coinConfig);
if (abi) {
Expand Down Expand Up @@ -72,6 +78,10 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
return this.initializeBuilder(tx, new DigitalAssetTransferBuilder(this._coinConfig));
}

getDelegationPoolAddStakeTransactionBuilder(tx?: Transaction): DelegationPoolAddStakeTransactionBuilder {
return this.initializeBuilder(tx, new DelegationPoolAddStakeTransactionBuilder(this._coinConfig));
}

/**
* Get a custom transaction builder
*
Expand Down
3 changes: 3 additions & 0 deletions modules/sdk-coin-apt/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
APT_TRANSACTION_ID_LENGTH,
COIN_BATCH_TRANSFER_FUNCTION,
COIN_TRANSFER_FUNCTION,
DELEGATION_POOL_ADD_STAKE_FUNCTION,
DIGITAL_ASSET_TRANSFER_FUNCTION,
FUNGIBLE_ASSET_BATCH_TRANSFER_FUNCTION,
FUNGIBLE_ASSET_TRANSFER_FUNCTION,
Expand Down Expand Up @@ -97,6 +98,8 @@ export class Utils implements BaseUtils {
return TransactionType.SendToken;
case DIGITAL_ASSET_TRANSFER_FUNCTION:
return TransactionType.SendNFT;
case DELEGATION_POOL_ADD_STAKE_FUNCTION:
return TransactionType.StakingDelegate;
default:
// For any other function calls, treat as a custom transaction
return TransactionType.CustomTx;
Expand Down
13 changes: 13 additions & 0 deletions modules/sdk-coin-apt/test/resources/apt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,13 @@ export const digitalTokenRecipients: Recipient[] = [
},
];

export const delegationPoolAddStakeRecipients: Recipient[] = [
{
address: addresses.validAddresses[0],
amount: AMOUNT.toString(),
},
];

export const invalidRecipients: Recipient[] = [
{
address: addresses.invalidAddresses[0],
Expand Down Expand Up @@ -135,3 +142,9 @@ export const FUNGIBLE_BATCH_RAW_TX_HEX =

export const FUNGIBLE_BATCH_SIGNABLE_PAYLOAD =
'5efa3c4f02f83a0f4b2d69fc95c607cc02825cc4e7be536ef0992df050d9e67c011aed808916ab9b1b30b07abb53561afd46847285ce28651221d406173a3724490e000000000000000200000000000000000000000000000000000000000000000000000000000000010d6170746f735f6163636f756e741e62617463685f7472616e736665725f66756e6769626c655f617373657473000320d5d0d561493ea2b9410f67da804653ae44e793c2423707d4f11edb2e381920504102dd52c0b72a73696b867d6571a308c413e43bff8f44956a5991abc4d50db0b8492a81760d52db9a96df2609860218214b6d1012e77e84a3fed5145a9a65bf6932110201000000000000000100000000000000400d03000000000064000000000000008b037d67000000000200dbc87a1c816d9bcd06b683c37e80c7162e4d48da7812198b830e4d5d8e0629f2';

export const DELEGATION_POOL_ADD_STAKE_TX_HEX =
'0x1aed808916ab9b1b30b07abb53561afd46847285ce28651221d406173a3724490e000000000000000200000000000000000000000000000000000000000000000000000000000000010f64656c65676174696f6e5f706f6f6c096164645f7374616b65000220f7405c28a02cf5bab4ea4498240bb3579db45951794eb1c843bef0534c093ad908e803000000000000400d03000000000064000000000000008b037d670000000002030020000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dbc87a1c816d9bcd06b683c37e80c7162e4d48da7812198b830e4d5d8e0629f2002000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000';

export const DELEGATION_POOL_ADD_STAKE_TX_HEX_SIGNABLE_PAYLOAD =
'5efa3c4f02f83a0f4b2d69fc95c607cc02825cc4e7be536ef0992df050d9e67c011aed808916ab9b1b30b07abb53561afd46847285ce28651221d406173a3724490e000000000000000200000000000000000000000000000000000000000000000000000000000000010f64656c65676174696f6e5f706f6f6c096164645f7374616b65000220f7405c28a02cf5bab4ea4498240bb3579db45951794eb1c843bef0534c093ad908e803000000000000400d03000000000064000000000000008b037d67000000000200dbc87a1c816d9bcd06b683c37e80c7162e4d48da7812198b830e4d5d8e0629f2';
34 changes: 34 additions & 0 deletions modules/sdk-coin-apt/test/unit/apt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
} from '@aptos-labs/ts-sdk';
import utils from '../../src/lib/utils';
import { AptCoin, coins, GasTankAccountCoin } from '@bitgo/statics';
import { DelegationPoolAddStakeTransaction } from '../../src/lib/transaction/delegationPoolAddStakeTransaction';

describe('APT:', function () {
let bitgo: TestBitGoAPI;
Expand Down Expand Up @@ -229,6 +230,39 @@ describe('APT:', function () {
});
});

it('should explain a staking delegate transaction', async function () {
const rawTx = testData.DELEGATION_POOL_ADD_STAKE_TX_HEX;
const transaction = new DelegationPoolAddStakeTransaction(coins.get('tapt'));
transaction.fromRawTransaction(rawTx);
const explainedTx = transaction.explainTransaction();
explainedTx.should.deepEqual({
displayOrder: [
'id',
'outputs',
'outputAmount',
'changeOutputs',
'changeAmount',
'fee',
'withdrawAmount',
'sender',
'type',
],
id: '0xc5b960d1bec149c77896344774352c61441307af564eaa8c84f857208e411bf3',
outputs: [
{
address: '0xf7405c28a02cf5bab4ea4498240bb3579db45951794eb1c843bef0534c093ad9',
amount: '1000',
},
],
outputAmount: '1000',
changeOutputs: [],
changeAmount: '0',
fee: { fee: '0' },
sender: '0x1aed808916ab9b1b30b07abb53561afd46847285ce28651221d406173a372449',
type: 30,
});
});

it('should fail to explain a invalid raw transaction', async function () {
const rawTx = 'invalidRawTransaction';
const transaction = new TransferTransaction(coins.get('tapt'));
Expand Down
Loading