Skip to content

Commit

Permalink
[TS SDK] Support transfer fungible token (#8631)
Browse files Browse the repository at this point in the history
* support fungible token transfer operation

* support fungible token transfer operation

* support fungible token transfer operation

* update comments

* fix lint

* query indexer to check ir token is nft or ft

* note only support tokenv2 standard
  • Loading branch information
0xmaayan authored Jun 22, 2023
1 parent 66de618 commit a132c16
Show file tree
Hide file tree
Showing 10 changed files with 261 additions and 54 deletions.
3 changes: 3 additions & 0 deletions ecosystem/typescript/sdk/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ All notable changes to the Aptos Node SDK will be captured in this file. This ch
- export `bcsSerializeU256` from `bcs/helper.ts`
- Add `examples` folder to `.npmignore`
- use `0x1::aptos_account::transfer` in tests
- Support transfer a fungible token.
- add a `transfer` function to the `AptosToken` class that accepts `NonFungibleTokenParameters` or `FungibleTokenParameters` types.
- `getTokenData` query supports token standard v2. Return fields have changed.

## 1.10.0 (2023-06-07)

Expand Down
4 changes: 2 additions & 2 deletions ecosystem/typescript/sdk/src/indexer/generated/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,11 +111,11 @@ export type GetTokenActivitiesCountQueryVariables = Types.Exact<{
export type GetTokenActivitiesCountQuery = { __typename?: 'query_root', token_activities_aggregate: { __typename?: 'token_activities_aggregate', aggregate?: { __typename?: 'token_activities_aggregate_fields', count: number } | null } };

export type GetTokenDataQueryVariables = Types.Exact<{
token_id?: Types.InputMaybe<Types.Scalars['String']>;
where_condition?: Types.InputMaybe<Types.Current_Token_Datas_V2_Bool_Exp>;
}>;


export type GetTokenDataQuery = { __typename?: 'query_root', current_token_datas: Array<{ __typename?: 'current_token_datas', token_data_id_hash: string, name: string, collection_name: string, creator_address: string, default_properties: any, largest_property_version: any, maximum: any, metadata_uri: string, payee_address: string, royalty_points_denominator: any, royalty_points_numerator: any, supply: any }> };
export type GetTokenDataQuery = { __typename?: 'query_root', current_token_datas_v2: Array<{ __typename?: 'current_token_datas_v2', token_data_id: string, token_name: string, token_uri: string, token_properties: any, token_standard: string, largest_property_version_v1?: any | null, maximum?: any | null, is_fungible_v2?: boolean | null, supply: any, last_transaction_version: any, last_transaction_timestamp: any, current_collection?: { __typename?: 'current_collections_v2', collection_id: string, collection_name: string, creator_address: string, uri: string, current_supply: any } | null }> };

export type GetTokenOwnedFromCollectionQueryVariables = Types.Exact<{
where_condition: Types.Current_Token_Ownerships_V2_Bool_Exp;
Expand Down
30 changes: 18 additions & 12 deletions ecosystem/typescript/sdk/src/indexer/generated/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,20 +250,26 @@ export const GetTokenActivitiesCount = `
}
`;
export const GetTokenData = `
query getTokenData($token_id: String) {
current_token_datas(where: {token_data_id_hash: {_eq: $token_id}}) {
token_data_id_hash
name
collection_name
creator_address
default_properties
largest_property_version
query getTokenData($where_condition: current_token_datas_v2_bool_exp) {
current_token_datas_v2(where: $where_condition) {
token_data_id
token_name
token_uri
token_properties
token_standard
largest_property_version_v1
maximum
metadata_uri
payee_address
royalty_points_denominator
royalty_points_numerator
is_fungible_v2
supply
last_transaction_version
last_transaction_timestamp
current_collection {
collection_id
collection_name
creator_address
uri
current_supply
}
}
}
`;
Expand Down
6 changes: 6 additions & 0 deletions ecosystem/typescript/sdk/src/indexer/generated/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3677,6 +3677,7 @@ export enum Order_By {
export type Processor_Status = {
__typename?: 'processor_status';
last_success_version: Scalars['bigint'];
last_updated: Scalars['timestamp'];
processor: Scalars['String'];
};

Expand All @@ -3686,12 +3687,14 @@ export type Processor_Status_Bool_Exp = {
_not?: InputMaybe<Processor_Status_Bool_Exp>;
_or?: InputMaybe<Array<Processor_Status_Bool_Exp>>;
last_success_version?: InputMaybe<Bigint_Comparison_Exp>;
last_updated?: InputMaybe<Timestamp_Comparison_Exp>;
processor?: InputMaybe<String_Comparison_Exp>;
};

/** Ordering options when selecting data from "processor_status". */
export type Processor_Status_Order_By = {
last_success_version?: InputMaybe<Order_By>;
last_updated?: InputMaybe<Order_By>;
processor?: InputMaybe<Order_By>;
};

Expand All @@ -3700,6 +3703,8 @@ export enum Processor_Status_Select_Column {
/** column name */
LastSuccessVersion = 'last_success_version',
/** column name */
LastUpdated = 'last_updated',
/** column name */
Processor = 'processor'
}

Expand All @@ -3714,6 +3719,7 @@ export type Processor_Status_Stream_Cursor_Input = {
/** Initial value of the column from where the streaming should start */
export type Processor_Status_Stream_Cursor_Value_Input = {
last_success_version?: InputMaybe<Scalars['bigint']>;
last_updated?: InputMaybe<Scalars['timestamp']>;
processor?: InputMaybe<Scalars['String']>;
};

Expand Down
30 changes: 18 additions & 12 deletions ecosystem/typescript/sdk/src/indexer/queries/getTokenData.graphql
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
query getTokenData($token_id: String) {
current_token_datas(where: { token_data_id_hash: { _eq: $token_id } }) {
token_data_id_hash
name
collection_name
creator_address
default_properties
largest_property_version
query getTokenData($where_condition: current_token_datas_v2_bool_exp) {
current_token_datas_v2(where: $where_condition) {
token_data_id
token_name
token_uri
token_properties
token_standard
largest_property_version_v1
maximum
metadata_uri
payee_address
royalty_points_denominator
royalty_points_numerator
is_fungible_v2
supply
last_transaction_version
last_transaction_timestamp
current_collection {
collection_id
collection_name
creator_address
uri
current_supply
}
}
}
68 changes: 65 additions & 3 deletions ecosystem/typescript/sdk/src/plugins/aptos_token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { AptosClient, OptionalTransactionArgs } from "../providers/aptos_client"
import { TransactionBuilderRemoteABI } from "../transaction_builder";
import { HexString, MaybeHexString } from "../utils";
import { getPropertyValueRaw, getSinglePropertyValueRaw } from "../utils/property_map_serde";
import { FungibleAssetClient } from "./fungible_asset_client";

export interface CreateCollectionOptions {
royaltyNumerator?: number;
Expand Down Expand Up @@ -41,6 +42,22 @@ const PropertyTypeMap = {

export type PropertyType = keyof typeof PropertyTypeMap;

type FungibleTokenParameters = {
owner: AptosAccount;
tokenAddress: MaybeHexString;
recipient: MaybeHexString;
amount: number | bigint;
extraArgs?: OptionalTransactionArgs;
};

type NonFungibleTokenParameters = {
owner: AptosAccount;
tokenAddress: MaybeHexString;
recipient: MaybeHexString;
tokenType?: string;
extraArgs?: OptionalTransactionArgs;
};

/**
* Class for managing aptos_token
*/
Expand Down Expand Up @@ -460,7 +477,7 @@ export class AptosToken {
}

/**
* Transfer a token ownership.
* Transfer a non fungible token ownership.
* We can transfer a token only when the token is not frozen (i.e. owner transfer is not disabled such as for soul bound tokens)
* @param owner The account of the current token owner
* @param token Token address
Expand All @@ -474,7 +491,7 @@ export class AptosToken {
tokenType?: string,
extraArgs?: OptionalTransactionArgs,
): Promise<string> {
const builder = new TransactionBuilderRemoteABI(this.provider.aptosClient, {
const builder = new TransactionBuilderRemoteABI(this.provider, {
sender: owner.address(),
...extraArgs,
});
Expand All @@ -484,7 +501,52 @@ export class AptosToken {
[HexString.ensure(token).hex(), HexString.ensure(recipient).hex()],
);
const bcsTxn = AptosClient.generateBCSTransaction(owner, rawTxn);
const pendingTransaction = await this.provider.aptosClient.submitSignedBCSTransaction(bcsTxn);
const pendingTransaction = await this.provider.submitSignedBCSTransaction(bcsTxn);
return pendingTransaction.hash;
}

/**
* Transfer a token. This function supports transfer non-fungible token and fungible token.
*
* To set the token type, set isFungibleToken param to true or false.
* If isFungibleToken param is not set, the function would query Indexer
* for the token data and check whether it is a non-fungible or a fungible token.
*
* Note: this function supports only token v2 standard (it does not support the token v1 standard)
*
* @param data NonFungibleTokenParameters | FungibleTokenParameters type
* @param isFungibleToken (optional) The token type, non-fungible or fungible token.
* @returns The hash of the transaction submitted to the API
*/
async transfer(
data: NonFungibleTokenParameters | FungibleTokenParameters,
isFungibleToken?: boolean | null,
): Promise<string> {
let isFungible = isFungibleToken;
if (isFungible === undefined || isFungible === null) {
const tokenData = await this.provider.getTokenData(HexString.ensure(data.tokenAddress).hex());
isFungible = tokenData.current_token_datas_v2[0].is_fungible_v2;
}
if (isFungible) {
const token = data as FungibleTokenParameters;
const fungibleAsset = new FungibleAssetClient(this.provider);
const txnHash = await fungibleAsset.transfer(
token.owner,
token.tokenAddress,
token.recipient,
token.amount,
token.extraArgs,
);
return txnHash;
}
const token = data as NonFungibleTokenParameters;
const txnHash = await this.transferTokenOwnership(
token.owner,
token.tokenAddress,
token.recipient,
token.tokenType,
token.extraArgs,
);
return txnHash;
}
}
21 changes: 18 additions & 3 deletions ecosystem/typescript/sdk/src/providers/indexer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,13 +255,28 @@ export class IndexerClient {
/**
* Queries token data
*
* @param tokenId Token ID
* @param tokenId Token ID address
* @returns GetTokenDataQuery response type
*/
async getTokenData(tokenId: string): Promise<GetTokenDataQuery> {
async getTokenData(
tokenId: string,
extraArgs?: {
tokenStandard?: TokenStandard;
},
): Promise<GetTokenDataQuery> {
const tokenAddress = HexString.ensure(tokenId).hex();
IndexerClient.validateAddress(tokenAddress);

const whereCondition: any = {
token_data_id: { _eq: tokenAddress },
};

if (extraArgs?.tokenStandard) {
whereCondition.token_standard = { _eq: extraArgs?.tokenStandard };
}
const graphqlQuery = {
query: GetTokenData,
variables: { token_id: tokenId },
variables: { where_condition: whereCondition },
};
return this.queryIndexer(graphqlQuery);
}
Expand Down
44 changes: 44 additions & 0 deletions ecosystem/typescript/sdk/src/tests/e2e/aptos_token.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ describe("token objects", () => {
beforeAll(async () => {
// Fund Alice's Account
await faucetClient.fundAccount(alice.address(), 100000000);
await faucetClient.fundAccount(bob.address(), 100000000);
}, longTestTimeout);

test(
Expand Down Expand Up @@ -193,6 +194,49 @@ describe("token objects", () => {
longTestTimeout,
);

test(
"transfer non fungible token",
async () => {
const getTokenDataSpy = jest.spyOn(provider, "getTokenData");
const getTokenDataSpyResponse = { current_token_datas_v2: new Array() };
getTokenDataSpyResponse.current_token_datas_v2.push({ is_fungible_v2: undefined });
getTokenDataSpy.mockResolvedValue(getTokenDataSpyResponse);

await provider.waitForTransaction(
await aptosToken.transfer({ owner: bob, tokenAddress, recipient: alice.address() }),
{
checkSuccess: true,
},
);
getTokenDataSpy.mockRestore();
},
longTestTimeout,
);

test(
"transfer non fungible token when isFungibleToken param set to false",
async () => {
await provider.waitForTransaction(
await aptosToken.transfer({ owner: alice, tokenAddress, recipient: bob.address() }, false),
{
checkSuccess: true,
},
);
},
longTestTimeout,
);

test(
"getTokenData indexer query is not being called when isFungibleToken param is set",
async () => {
const getTokenDataSpy = jest.spyOn(provider, "getTokenData");
await aptosToken.transfer({ owner: bob, tokenAddress, recipient: alice.address() }, false);
expect(getTokenDataSpy).not.toBeCalled();
getTokenDataSpy.mockRestore();
},
longTestTimeout,
);

test(
"burn token",
async () => {
Expand Down
Loading

0 comments on commit a132c16

Please sign in to comment.