Skip to content

Commit

Permalink
Merge pull request #5440 from BitGo/COIN-2920-apt-nft-transfer
Browse files Browse the repository at this point in the history
feat(sdk-coin-apt): non fungible assest transfer
  • Loading branch information
bhavidhingra authored Jan 31, 2025
2 parents 725c495 + eb84c77 commit 3b4f8e2
Show file tree
Hide file tree
Showing 10 changed files with 7,138 additions and 4,868 deletions.
7 changes: 4 additions & 3 deletions modules/sdk-coin-apt/src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ export const UNAVAILABLE_TEXT = 'UNAVAILABLE';
export const DEFAULT_GAS_UNIT_PRICE = 100;
export const SECONDS_PER_WEEK = 7 * 24 * 60 * 60; // Days * Hours * Minutes * Seconds

export const APTOS_ACCOUNT_MODULE = 'aptos_account';
export const FUNGIBLE_ASSET_MODULE = 'primary_fungible_store';
export const DIGITAL_ASSET_TRANSFER_AMOUNT = '1';

export const FUNGIBLE_ASSET_TRANSFER_FUNCTION = '0x1::primary_fungible_store::transfer';
export const COIN_TRANSFER_FUNCTION = '0x1::aptos_account::transfer_coins';
export const DIGITAL_ASSET_TRANSFER_FUNCTION = '0x1::object::transfer';

export const FUNGIBLE_ASSET_TYPE_ARGUMENT = '0x1::fungible_asset::Metadata';
export const APTOS_COIN = '0x1::aptos_coin::AptosCoin';
export const FUNGIBLE_ASSET_TYPE_ARGUMENT = '0x1::fungible_asset::Metadata';
export const DIGITAL_ASSET_TYPE_ARGUMENT = '0x4::token::Token';
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { Transaction } from './transaction';
import {
AccountAddress,
Aptos,
AptosConfig,
EntryFunctionABI,
MoveAbility,
Network,
objectStructTag,
TransactionPayload,
TransactionPayloadEntryFunction,
TypeTagAddress,
TypeTagGeneric,
TypeTagStruct,
} from '@aptos-labs/ts-sdk';
import { InvalidTransactionError, TransactionRecipient, TransactionType } from '@bitgo/sdk-core';
import { BaseCoin as CoinConfig, NetworkType } from '@bitgo/statics';
import {
DIGITAL_ASSET_TYPE_ARGUMENT,
DIGITAL_ASSET_TRANSFER_FUNCTION,
DIGITAL_ASSET_TRANSFER_AMOUNT,
} from '../constants';

export class DigitalAssetTransaction extends Transaction {
constructor(coinConfig: Readonly<CoinConfig>) {
super(coinConfig);
this._type = TransactionType.SendNFT;
}

protected parseTransactionPayload(payload: TransactionPayload): void {
if (
!(payload instanceof TransactionPayloadEntryFunction) ||
payload.entryFunction.args.length !== 2 ||
payload.entryFunction.type_args.length !== 1 ||
DIGITAL_ASSET_TYPE_ARGUMENT !== payload.entryFunction.type_args[0].toString()
) {
throw new InvalidTransactionError('Invalid transaction payload');
}
const entryFunction = payload.entryFunction;
if (!this._recipient) {
this._recipient = {} as TransactionRecipient;
}
this._assetId = entryFunction.args[0].toString();
this._recipient.address = entryFunction.args[1].toString();
this._recipient.amount = DIGITAL_ASSET_TRANSFER_AMOUNT;
}

protected async buildRawTransaction(): Promise<void> {
const network: Network = this._coinConfig.network.type === NetworkType.MAINNET ? Network.MAINNET : Network.TESTNET;
const aptos = new Aptos(new AptosConfig({ network }));
const senderAddress = AccountAddress.fromString(this._sender);
const recipientAddress = AccountAddress.fromString(this._recipient.address);
const digitalAssetAddress = AccountAddress.fromString(this._assetId);

const transferDigitalAssetAbi: EntryFunctionABI = {
typeParameters: [{ constraints: [MoveAbility.KEY] }],
parameters: [new TypeTagStruct(objectStructTag(new TypeTagGeneric(0))), new TypeTagAddress()],
};

const simpleTxn = await aptos.transaction.build.simple({
sender: senderAddress,
data: {
function: DIGITAL_ASSET_TRANSFER_FUNCTION,
typeArguments: [DIGITAL_ASSET_TYPE_ARGUMENT],
functionArguments: [digitalAssetAddress, recipientAddress],
abi: transferDigitalAssetAbi,
},
options: {
maxGasAmount: this.maxGasAmount,
gasUnitPrice: this.gasUnitPrice,
expireTimestamp: this.expirationTime,
accountSequenceNumber: this.sequenceNumber,
},
});
this._rawTransaction = simpleTxn.rawTransaction;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
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 { DigitalAssetTransaction } from '../transaction/digitalAssetTransaction';
import { DIGITAL_ASSET_TYPE_ARGUMENT } from '../constants';

export class DigitalAssetTransactionBuilder extends TransactionBuilder {
constructor(_coinConfig: Readonly<CoinConfig>) {
super(_coinConfig);
this._transaction = new DigitalAssetTransaction(_coinConfig);
}

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

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

/** @inheritdoc */
validateTransaction(transaction?: DigitalAssetTransaction): void {
if (!transaction) {
throw new Error('transaction not defined');
}
super.validateTransaction(transaction);
this.validateAddress({ address: transaction.assetId });
}

protected isValidTransactionPayload(payload: TransactionPayload) {
try {
if (
!(payload instanceof TransactionPayloadEntryFunction) ||
payload.entryFunction.args.length !== 2 ||
payload.entryFunction.type_args.length !== 1 ||
DIGITAL_ASSET_TYPE_ARGUMENT !== payload.entryFunction.type_args[0].toString()
) {
console.error('invalid transaction payload');
return false;
}
const entryFunction = payload.entryFunction;
const digitalAssetAddress = entryFunction.args[0].toString();
const recipientAddress = entryFunction.args[1].toString();
return utils.isValidAddress(recipientAddress) && utils.isValidAddress(digitalAssetAddress);
} catch (e) {
console.error('invalid transaction payload', e);
return false;
}
}
}
11 changes: 11 additions & 0 deletions modules/sdk-coin-apt/src/lib/transactionBuilderFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { TransferTransaction } from './transaction/transferTransaction';
import { BaseCoin as CoinConfig } from '@bitgo/statics';
import { FungibleAssetTransaction } from './transaction/fungibleAssetTransaction';
import { FungibleAssetTransactionBuilder } from './transactionBuilder/fungibleAssetTransactionBuilder';
import { DigitalAssetTransaction } from './transaction/digitalAssetTransaction';
import { DigitalAssetTransactionBuilder } from './transactionBuilder/digitalAssestTransactionBuilder';

export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
constructor(_coinConfig: Readonly<CoinConfig>) {
Expand All @@ -28,6 +30,10 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
const fungibleTransferTokenTx = new FungibleAssetTransaction(this._coinConfig);
fungibleTransferTokenTx.fromDeserializedSignedTransaction(signedTxn);
return this.getFungibleAssetTransactionBuilder(fungibleTransferTokenTx);
case TransactionType.SendNFT:
const digitalAssetTransferTx = new DigitalAssetTransaction(this._coinConfig);
digitalAssetTransferTx.fromDeserializedSignedTransaction(signedTxn);
return this.getDigitalAssetTransactionBuilder(digitalAssetTransferTx);
default:
throw new InvalidTransactionError('Invalid transaction');
}
Expand All @@ -51,6 +57,11 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
return this.initializeBuilder(tx, new FungibleAssetTransactionBuilder(this._coinConfig));
}

/** @inheritdoc */
getDigitalAssetTransactionBuilder(tx?: Transaction): DigitalAssetTransactionBuilder {
return this.initializeBuilder(tx, new DigitalAssetTransactionBuilder(this._coinConfig));
}

/** @inheritdoc */
getWalletInitializationBuilder(): void {
throw new Error('Method not implemented.');
Expand Down
16 changes: 11 additions & 5 deletions modules/sdk-coin-apt/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ import {
APT_BLOCK_ID_LENGTH,
APT_SIGNATURE_LENGTH,
APT_TRANSACTION_ID_LENGTH,
APTOS_ACCOUNT_MODULE,
FUNGIBLE_ASSET_MODULE,
COIN_TRANSFER_FUNCTION,
DIGITAL_ASSET_TRANSFER_FUNCTION,
FUNGIBLE_ASSET_TRANSFER_FUNCTION,
} from './constants';
import BigNumber from 'bignumber.js';

Expand Down Expand Up @@ -72,12 +73,17 @@ export class Utils implements BaseUtils {
throw new Error('Invalid Payload: Expected TransactionPayloadEntryFunction');
}
const entryFunction = payload.entryFunction;
const moduleAddress = entryFunction.module_name.address.toString();
const moduleIdentifier = entryFunction.module_name.name.identifier;
switch (moduleIdentifier) {
case APTOS_ACCOUNT_MODULE:
const functionIdentifier = entryFunction.function_name.identifier;
const uniqueIdentifier = `${moduleAddress}::${moduleIdentifier}::${functionIdentifier}`;
switch (uniqueIdentifier) {
case COIN_TRANSFER_FUNCTION:
return TransactionType.Send;
case FUNGIBLE_ASSET_MODULE:
case FUNGIBLE_ASSET_TRANSFER_FUNCTION:
return TransactionType.SendToken;
case DIGITAL_ASSET_TRANSFER_FUNCTION:
return TransactionType.SendNFT;
default:
throw new InvalidTransactionError(`Invalid transaction: unable to fetch transaction type ${moduleIdentifier}`);
}
Expand Down
16 changes: 14 additions & 2 deletions modules/sdk-coin-apt/test/resources/apt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ export const fungibleTokenRecipients: Recipient[] = [
},
];

export const digitalTokenRecipients: Recipient[] = [
{
address: addresses.validAddresses[0],
amount: '1',
},
];

export const invalidRecipients: Recipient[] = [
{
address: addresses.invalidAddresses[0],
Expand All @@ -67,14 +74,19 @@ export const TRANSFER =
export const TRANSACTION_USING_TRANSFER_COINS =
'0x1aed808916ab9b1b30b07abb53561afd46847285ce28651221d406173a37244992000000000000000200000000000000000000000000000000000000000000000000000000000000010d6170746f735f6163636f756e740e7472616e736665725f636f696e73010700000000000000000000000000000000000000000000000000000000000000010a6170746f735f636f696e094170746f73436f696e000220f7405c28a02cf5bab4ea4498240bb3579db45951794eb1c843bef0534c093ad908e803000000000000400d0300000000006400000000000000979390670000000002030020f73836f42257240e43d439552471fc9dbcc3f1af5bd0b4ed83f44b5f6614644240caeb90efd4b7ecd922c97bb3163e6a9de1fbb2ee0fc0d56af484f4af9b0015c5831341550af29b3686713b6657c821d894635fe13c7933f06ee043728f040b090000dbc87a1c816d9bcd06b683c37e80c7162e4d48da7812198b830e4d5d8e0629f200205223396c531f13e031a9f0cb26d459d799a52e51be9a1cb9e871afb4c31f91ff4013e7e8a1325ee5f656c93baa3d0206a1d9bd6da5abdc6f5d9b8bbbb0926ddac68f3e57a915dd217d2d43e776a6cc01af72f895ea712acc836d30349f29a3c606';

export const FUNGIBLE_TOKEN_TRANSFER =
'0x1aed808916ab9b1b30b07abb53561afd46847285ce28651221d406173a372449a700000000000000020000000000000000000000000000000000000000000000000000000000000001167072696d6172795f66756e6769626c655f73746f7265087472616e73666572010700000000000000000000000000000000000000000000000000000000000000010e66756e6769626c655f6173736574084d65746164617461000320d5d0d561493ea2b9410f67da804653ae44e793c2423707d4f11edb2e3819205020f7405c28a02cf5bab4ea4498240bb3579db45951794eb1c843bef0534c093ad9080100000000000000400d0300000000006400000000000000e42696670000000002030020f73836f42257240e43d439552471fc9dbcc3f1af5bd0b4ed83f44b5f661464424029665cd4c94658a0d83907bbed7e761794b25bccc03fc87e6dd63a543accdddfd7a6f1e7a15e8681547ca437ff99b58c92f816e35a0f333d7f1fd1330c21ad0a0000dbc87a1c816d9bcd06b683c37e80c7162e4d48da7812198b830e4d5d8e0629f200205223396c531f13e031a9f0cb26d459d799a52e51be9a1cb9e871afb4c31f91ff40de7b0bb86ca346031942e9cf21ff9604c7c08c2c662e38a0afe3dd640512c0441396c0697cd8bbbcf39694d6f88e35f6bed9fb34bd209b0479b5e8bd0cf3eb0b';

export const DIGITAL_ASSET_TRANSFER =
'0x1aed808916ab9b1b30b07abb53561afd46847285ce28651221d406173a372449ab00000000000000020000000000000000000000000000000000000000000000000000000000000001066f626a656374087472616e736665720107000000000000000000000000000000000000000000000000000000000000000405746f6b656e05546f6b656e0002202e356062777469d39ca5d9b72512ce2d5713d7938ed6ca9193d4fc2016a819fd20f7405c28a02cf5bab4ea4498240bb3579db45951794eb1c843bef0534c093ad9400d0300000000006400000000000000526798670000000002030020f73836f42257240e43d439552471fc9dbcc3f1af5bd0b4ed83f44b5f66146442400c2c31eae495d22d1d31031b9756e8238a3df6e760515aa3e1108d93b3aec6aeb91684307e8365ad9dc44c0b5957e95a21a6f47d8d4a6e0eb3b145fb3d517f030000dbc87a1c816d9bcd06b683c37e80c7162e4d48da7812198b830e4d5d8e0629f200205223396c531f13e031a9f0cb26d459d799a52e51be9a1cb9e871afb4c31f91ff40dd6c064e44642819dec2f63d32d6daa4f889d62d06025ad99b42562c4c6cdef8b1437739115d7f38050078829efb9dd05528f53309bcab5b89fadb283423100d';

export const INVALID_TRANSFER =
'AAAAAAAAAAAAA6e7361637469bc4a58e500b9e64cb6547ee9b403000000000000002064ba1fb2f2fbd2938a350015d601f4db89cd7e8e2370d0dd9ae3ac4f635c1581111b8a49f67370bc4a58e500b9e64cb6462e39b802000000000000002064ba1fb2f2fbd2938a350015d601f4db89cd7e8e2370d0dd9ae3ac47aa1ff81f01c4173a804406a365e69dfb297d4eaaf002546ebd016400000000000000cba4a48bb0f8b586c167e5dcefaa1c5e96ab3f0836d6ca08f2081732944d1e5b6b406a4a462e39b8030000000000000020b9490ede63215262c434e03f606d9799f3ba704523ceda184b386d47aa1ff81f01000000000000006400000000000000';

export const fungibleTokenAddress = {
usdt: '0xd5d0d561493ea2b9410f67da804653ae44e793c2423707d4f11edb2e38192050',
};

export const FUNGIBLE_TOKEN_TRANSFER =
'0x1aed808916ab9b1b30b07abb53561afd46847285ce28651221d406173a372449a700000000000000020000000000000000000000000000000000000000000000000000000000000001167072696d6172795f66756e6769626c655f73746f7265087472616e73666572010700000000000000000000000000000000000000000000000000000000000000010e66756e6769626c655f6173736574084d65746164617461000320d5d0d561493ea2b9410f67da804653ae44e793c2423707d4f11edb2e3819205020f7405c28a02cf5bab4ea4498240bb3579db45951794eb1c843bef0534c093ad9080100000000000000400d0300000000006400000000000000e42696670000000002030020f73836f42257240e43d439552471fc9dbcc3f1af5bd0b4ed83f44b5f661464424029665cd4c94658a0d83907bbed7e761794b25bccc03fc87e6dd63a543accdddfd7a6f1e7a15e8681547ca437ff99b58c92f816e35a0f333d7f1fd1330c21ad0a0000dbc87a1c816d9bcd06b683c37e80c7162e4d48da7812198b830e4d5d8e0629f200205223396c531f13e031a9f0cb26d459d799a52e51be9a1cb9e871afb4c31f91ff40de7b0bb86ca346031942e9cf21ff9604c7c08c2c662e38a0afe3dd640512c0441396c0697cd8bbbcf39694d6f88e35f6bed9fb34bd209b0479b5e8bd0cf3eb0b';
export const digitalAssetAddress = '0x2e356062777469d39ca5d9b72512ce2d5713d7938ed6ca9193d4fc2016a819fd';

export const LEGACY_COIN = '0x4fb379c10c763a13e724064ecfb7d946690bea519ba982c81b518d1c11dd23fe::fa_test::Coinz';
4 changes: 2 additions & 2 deletions modules/sdk-coin-apt/test/unit/apt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ describe('APT:', function () {
let newTxParams;

const txPreBuild = {
txHex: testData.TRANSFER,
txHex: testData.TRANSACTION_USING_TRANSFER_COINS,
txInfo: {},
};

Expand Down Expand Up @@ -118,7 +118,7 @@ describe('APT:', function () {

it('should parse a transfer transaction', async function () {
const parsedTransaction = await basecoin.parseTransaction({
txHex: testData.TRANSFER,
txHex: testData.TRANSACTION_USING_TRANSFER_COINS,
});

parsedTransaction.should.deepEqual({
Expand Down
Loading

0 comments on commit 3b4f8e2

Please sign in to comment.