From c416eff973e2163527a23d81c310c87824437e34 Mon Sep 17 00:00:00 2001 From: Daniel Porteous Date: Thu, 25 Aug 2022 15:07:42 -0700 Subject: [PATCH] Add TS to the first NFT SDK tutorial --- .../docs/tutorials/first-transaction-sdk.md | 6 +- .../docs/tutorials/your-first-nft-sdk.md | 258 +++++++++++++----- developer-docs-site/docs/whats-new-in-docs.md | 4 +- ecosystem/python/sdk/examples/simple-nft.py | 2 +- ecosystem/typescript/sdk/CHANGELOG.md | 1 + .../sdk/examples/typescript/package.json | 7 +- .../sdk/examples/typescript/simple_nft.ts | 163 +++++++++++ .../sdk/examples/typescript/transfer_coin.ts | 2 +- ecosystem/typescript/sdk/src/abis.ts | 4 + .../typescript/sdk/src/aptos_client.test.ts | 59 +--- ecosystem/typescript/sdk/src/token_client.ts | 88 +++++- 11 files changed, 459 insertions(+), 135 deletions(-) create mode 100644 ecosystem/typescript/sdk/examples/typescript/simple_nft.ts diff --git a/developer-docs-site/docs/tutorials/first-transaction-sdk.md b/developer-docs-site/docs/tutorials/first-transaction-sdk.md index 42ac5be50fb8f..154fdb4bc1d74 100644 --- a/developer-docs-site/docs/tutorials/first-transaction-sdk.md +++ b/developer-docs-site/docs/tutorials/first-transaction-sdk.md @@ -118,7 +118,7 @@ The `transfer-coin` example code uses helper functions to interact with the [RES :::tip See the full example -See [`transfer-coin`](https://github.com/aptos-labs/aptos-core/blob/main/ecosystem/typescript/sdk/examples/typescript/transfer_coin.ts) for the complete code as you follow the below steps. +See [`transfer_coin`](https://github.com/aptos-labs/aptos-core/blob/main/ecosystem/typescript/sdk/examples/typescript/transfer_coin.ts) for the complete code as you follow the below steps. ::: @@ -137,9 +137,9 @@ See [`transfer-coin`](https://github.com/aptos-labs/aptos-core/blob/main/sdk/exa ### Step 4.1: Initializing the Clients -In the first step, the `transfer-coin` example initializes both the REST and faucet clients. +In the first step, the `transfer-coin` example initializes both the API and faucet clients. -- The REST client interacts with the REST API, and +- The API client interacts with the REST API, and - The faucet client interacts with the devnet Faucet service for creating and funding accounts. diff --git a/developer-docs-site/docs/tutorials/your-first-nft-sdk.md b/developer-docs-site/docs/tutorials/your-first-nft-sdk.md index c2b92f22210dd..ee05c694eb8dd 100644 --- a/developer-docs-site/docs/tutorials/your-first-nft-sdk.md +++ b/developer-docs-site/docs/tutorials/your-first-nft-sdk.md @@ -14,18 +14,58 @@ This tutorial describes, in the following step-by-step approach, how to create a ## Step 1: Pick an SDK +* [Official Aptos Typescript SDK][typescript-sdk] * [Official Aptos Python SDK][python-sdk] -* Official Aptos Typescript SDK -- TBA * Official Aptos Rust SDK -- TBA ## Step 2: Run the Example Each SDK provides an examples directory. This tutorial covers the `simple-nft` example. - +Clone `aptos-core`: +```sh +git clone git@github.com:aptos-labs/aptos-core.git ~/aptos-core +``` + + + + + Navigate to the Typescript SDK examples directory: + ```sh + cd ~/aptos-core/ecosystem/typescript/sdk/examples/typescript + ``` + + Install the necessary dependencies: + ``` + yarn install + ``` + + Run the `simple_nft` example: + ```sh + yarn run simple_nft + ``` + -In the SDK directory run: `python -m examples.simple-nft` + Navigate to the Python SDK directory: + ```sh + cd ~/aptos-core/ecosystem/python/sdk + ``` + + Install the necessary dependencies: + ``` + curl -sSL https://install.python-poetry.org | python3 + poetry update + ``` + + Run the `transfer-coin` example: + ```sh + poetry run python -m examples.simple-nft + ``` + + + +Coming soon! @@ -38,7 +78,7 @@ The following output should appear after executing the `simple-nft` example, tho Alice: 0x9df0f527f3a0b445e4d5c320cfa269cdefafc7cd1ed17ffce4b3fd485b17aafb Bob: 0xfcc74af84dde26b0050dce35d6b3d11c60f5c8c58728ca3a0b11035942a0b1de -=== Initial Balances === +=== Initial Coin Balances === Alice: 20000 Bob: 20000 @@ -102,45 +142,86 @@ This example demonstrates: ## Step 4: The SDK in Depth + + + +:::tip See the full example +See [`simple_nft`](https://github.com/aptos-labs/aptos-core/blob/main/ecosystem/typescript/sdk/examples/typescript/simple_nft.ts) for the complete code as you follow the below steps. +::: + + + +:::tip See the full example +See [`simple-nft`](https://github.com/aptos-labs/aptos-core/blob/main/ecosystem/python/sdk/examples/simple-nft.py) for the complete code as you follow the below steps. +::: + + + +Coming soon! + + + ### Step 4.1: Initializing the Clients -In the first step, the example initializes both the REST and Faucet clients. The REST client interacts with the REST API, whereas the Faucet client interacts with the devnet Faucet service for creating and funding accounts. +In the first step the example initializes both the API and faucet clients. - +- The API client interacts with the REST API, and +- The faucet client interacts with the devnet Faucet service for creating and funding accounts. + + + + +```ts +:!: static/sdks/typescript/examples/typescript/simple_nft.ts section_1a +``` + +Using the API client we can create a `TokenClient`, which we use for common token operations such as creating collections and tokens, transferring them, claiming them, and so on. +```ts +:!: static/sdks/typescript/examples/typescript/simple_nft.ts section_1b +``` + +`common.ts` initializes the URL values as such: +```ts +:!: static/sdks/typescript/examples/typescript/common.ts section_1 +``` + ```python :!: static/sdks/python/examples/simple-nft.py section_1 ``` -`common.py` initializes these values as such: +[`common.py`](https://github.com/aptos-labs/aptos-core/tree/main/ecosystem/python/sdk/examples/common.py) initializes these values as follows: + ```python :!: static/sdks/python/examples/common.py section_1 ``` -In progress - - - -In progress +Coming soon! :::tip -The URLs for both services by default, point to our devnet services. However, they can be configured with the following environment variables: `APTOS_NODE_URL` and `APTOS_FAUCET_URL`. - +By default the URLs for both the services point to Aptos devnet services. However, they can be configured with the following environment variables: + - `APTOS_NODE_URL` + - `APTOS_FAUCET_URL` ::: ### Step 4.2: Creating local accounts -The next step is to create two accounts from the locally. [Accounts][account_basics] represent both on and off-chain state. Off-chain state consists of an -address and the public, private key pair used to authenticate ownership. This step demonstrates how to generate that off-chain state. +The next step is to create two accounts locally. [Accounts][account_basics] represent both on and off-chain state. Off-chain state consists of an address and the public, private key pair used to authenticate ownership. This step demonstrates how to generate that off-chain state. - + + + +```ts +:!: static/sdks/typescript/examples/typescript/simple_nft.ts section_2 +``` + ```python @@ -149,19 +230,21 @@ address and the public, private key pair used to authenticate ownership. This st -In progress - - - -In progress +Coming soon! ### Step 4.3: Creating blockchain accounts -In Aptos, each account must have an on-chain representation in order to support receive tokens and coins as well as interacting in other dApps. An account represents a medium for storing assets, hence it must be explicitly created. This example leverages the Faucet to create and fund Alice and Bob's account: +In Aptos, each account must have an on-chain representation in order to support receive tokens and coins as well as interacting in other dApps. An account represents a medium for storing assets, hence it must be explicitly created. This example leverages the Faucet to create Alice and Bob's accounts. Only Alice's is funded: - + + + +```ts +:!: static/sdks/typescript/examples/typescript/simple_nft.ts section_3 +``` + ```python @@ -170,11 +253,7 @@ In Aptos, each account must have an on-chain representation in order to support -In progress - - - -In progress +Coming soon! @@ -183,6 +262,18 @@ In progress Now begins the process of creating tokens. First, the creator must create a collection to store tokens. A collection can contain zero, one, or many distinct tokens within it. The collection does not restrict the attributes of the tokens, as it is only a container. + + +Your application will call `createCollection`: +```ts +:!: static/sdks/typescript/examples/typescript/simple_nft.ts section_4 +``` + +The function signature of `createCollection`. It returns a transaction hash: +```ts +:!: static/sdks/typescript/src/token_client.ts createCollection +``` + Your application will call `create_collection`: @@ -190,18 +281,14 @@ Your application will call `create_collection`: :!: static/sdks/python/examples/simple-nft.py section_4 ``` -The `create_collection`'s API, which returns a transaction hash: +The function signature of `create_collection`. It returns a transaction hash: ```python :!: static/sdks/python/aptos_sdk/client.py create_collection ``` -In progress - - - -In progress +Coming soon! @@ -210,6 +297,18 @@ In progress To create a token, the creator must specify an associated collection. A token must be associated with a collection and that collection must have remaining tokens that can be minted. There are many attributes associated with a token, but the helper API only exposes the minimal amount required to create static content. + + +Your application will call `createToken`: +```ts +:!: static/sdks/typescript/examples/typescript/simple_nft.ts section_5 +``` + +The function signature of `createToken`. It returns a transaction hash: +```ts +:!: static/sdks/typescript/src/token_client.ts createToken +``` + Your application will call `create_token`: @@ -217,18 +316,14 @@ Your application will call `create_token`: :!: static/sdks/python/examples/simple-nft.py section_5 ``` -`create_token`'s API, which returns a transaction hash: +The function signature of `create_token`. It returns a transaction hash: ```python :!: static/sdks/python/aptos_sdk/client.py create_token ``` -In progress - - - -In progress +Coming soon! @@ -237,6 +332,24 @@ In progress Both the collection and token metadata are stored on the creator's account within their `Collections` in a table. The SDKs provide convenience wrappers around querying these specific tables: + + +To read a collection's metadata: +```ts +:!: static/sdks/typescript/examples/typescript/simple_nft.ts section_6 +``` + +To read a token's metadata: +```ts +:!: static/sdks/typescript/examples/typescript/simple_nft.ts section_8 +``` + +Here's how `getTokenData` queries the token metadata: +```ts +:!: static/sdks/typescript/src/token_client.ts getTokenData +``` + + To read a collection's metadata: @@ -257,11 +370,7 @@ Here's how `get_token_data` queries the token metadata: -In progress - - - -In progress +Coming soon! @@ -270,6 +379,12 @@ In progress Each token within Aptos is a distinct asset, the assets owned by the user are stored within their `TokenStore`. To get the balance: + + +```ts +:!: static/sdks/typescript/examples/typescript/simple_nft.ts section_7 +``` + ```python @@ -278,11 +393,7 @@ Each token within Aptos is a distinct asset, the assets owned by the user are st -In progress - - - -In progress +Coming soon! @@ -293,6 +404,12 @@ Many users have received unwanted tokens that may cause minimally embarrassment To offer a token: + + +```ts +:!: static/sdks/typescript/examples/typescript/simple_nft.ts section_9 +``` + ```python @@ -301,17 +418,19 @@ To offer a token: -In progress - - - -In progress +Coming soon! To claim a token: + + +```ts +:!: static/sdks/typescript/examples/typescript/simple_nft.ts section_10 +``` + ```python @@ -320,11 +439,7 @@ To claim a token: -In progress - - - -In progress +Coming soon! @@ -332,6 +447,12 @@ In progress To support safe unilateral transfers of a token, the sender may first ask the recipient to acknowledge off-chain about a pending transfer. This comes in the form of a multiagent transaction request. Multiagent transactions contain multiple signatures, one for each on-chain account. Move then can leverage this to give `signer` level permissions to all that signed. For token transfers, this ensures that the receiving party does indeed desire to receive this token without requiring the use of the token transfer framework described above. + + +```ts +:!: static/sdks/typescript/examples/typescript/simple_nft.ts section_11 +``` + ```python @@ -340,33 +461,30 @@ To support safe unilateral transfers of a token, the sender may first ask the re -In progress - - - -In progress +Coming soon! ### Step 4.10: Enabling unilateral token transfers -In progress +Coming soon! -In progress +Coming soon! -In progress +Coming soon! -In progress +Coming soon! [account_basics]: /concepts/basics-accounts +[typescript-sdk]: /sdks/typescript-sdk [python-sdk]: /sdks/python-sdk [rest_spec]: https://fullnode.devnet.aptoslabs.com/v1/spec#/ diff --git a/developer-docs-site/docs/whats-new-in-docs.md b/developer-docs-site/docs/whats-new-in-docs.md index 9f5dedd5ca98c..f3dd03d7987a0 100644 --- a/developer-docs-site/docs/whats-new-in-docs.md +++ b/developer-docs-site/docs/whats-new-in-docs.md @@ -8,6 +8,8 @@ slug: "whats-new-in-docs" ## 24 August 2022 - The Korean language version of the [Aptos White Paper](/aptos-white-paper/aptos-white-paper-in-korean) is posted. +- Typescript and Rust are added to the [first transaction tutorial](/tutorials/your-first-transaction-sdk). +- A [new tutorial](/tutorials/your-first-nft-sdk) is added that shows you how to use the Typescript / Python SDKs to work with NFT. It covers topics such as creating your own collection, creating a token in that collection, and how to offer and claim that token. ## 16 August 2022 @@ -21,7 +23,7 @@ slug: "whats-new-in-docs" ## 08 August 2022 -- A new document for the [exploratory Move transactional testing](/guides/move-guides/guide-move-transactional-testing) is posted. +- A new document for the [exploratory Move transactional testing](/guides/move-guides/guide-move-transactional-testing) is posted. ## 07 August 2022 diff --git a/ecosystem/python/sdk/examples/simple-nft.py b/ecosystem/python/sdk/examples/simple-nft.py index e15dac4ae073e..a74e8c2051dbc 100644 --- a/ecosystem/python/sdk/examples/simple-nft.py +++ b/ecosystem/python/sdk/examples/simple-nft.py @@ -32,7 +32,7 @@ faucet_client.fund_account(bob.address(), 20_000) #<:!:section_3 - print("\n=== Initial Balances ===") + print("\n=== Initial Coin Balances ===") print(f"Alice: {rest_client.account_balance(alice.address())}") print(f"Bob: {rest_client.account_balance(bob.address())}") diff --git a/ecosystem/typescript/sdk/CHANGELOG.md b/ecosystem/typescript/sdk/CHANGELOG.md index 39d9c7a0f1762..e0de661ee5d71 100644 --- a/ecosystem/typescript/sdk/CHANGELOG.md +++ b/ecosystem/typescript/sdk/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to the Aptos Node SDK will be captured in this file. This ch ## Unreleased - **[Breaking Change]** Reimplemented the JSON transaction submission interfaces with BCS. This is a breaking change. `createSigningMessage` is removed. Before the changes, the transaction payloads take string aruguments. But now, Typescript payload arguments have to match the smart contract arugment types. e.g. `number` matches `u8`, `number | bigint` matches `u64` and `u128`, etc. +- **[Breaking Change]** `getTokenBalance` and `getTokenBalanceForAccount` have been renamed to `getToken` and `getTokenForAccount`, since they were never getting just the balance, but the full token. - Added `CoinClient` to help working with coins. This contains common operations such as `transfer`, `checkBalance`, etc. - Added `generateSignSendWaitForTransaction`, a function that provides a simple way to execute the full end to end transaction submission flow. diff --git a/ecosystem/typescript/sdk/examples/typescript/package.json b/ecosystem/typescript/sdk/examples/typescript/package.json index 26dd7711df56f..3de5a374b6eed 100644 --- a/ecosystem/typescript/sdk/examples/typescript/package.json +++ b/ecosystem/typescript/sdk/examples/typescript/package.json @@ -4,10 +4,11 @@ "description": "", "main": "index.js", "scripts": { - "test": "run-s transfer_coin bcs_transaction multisig_transaction", - "transfer_coin": "ts-node transfer_coin.ts", "bcs_transaction": "ts-node bcs_transaction.ts", - "multisig_transaction": "ts-node multisig_transaction.ts" + "multisig_transaction": "ts-node multisig_transaction.ts", + "simple_nft": "ts-node simple_nft.ts", + "transfer_coin": "ts-node transfer_coin.ts", + "test": "run-s bcs_transaction multisig_transaction simple_nft transfer_coin" }, "keywords": [], "author": "", diff --git a/ecosystem/typescript/sdk/examples/typescript/simple_nft.ts b/ecosystem/typescript/sdk/examples/typescript/simple_nft.ts new file mode 100644 index 0000000000000..2a3031124aa56 --- /dev/null +++ b/ecosystem/typescript/sdk/examples/typescript/simple_nft.ts @@ -0,0 +1,163 @@ +// Copyright (c) Aptos +// SPDX-License-Identifier: Apache-2.0 + +/* eslint-disable no-console */ + +import dotenv from "dotenv"; +dotenv.config(); + +import { AptosClient, AptosAccount, FaucetClient, TokenClient, CoinClient } from "aptos"; +import { NODE_URL, FAUCET_URL } from "./common"; + +(async () => { + // Create API and faucet clients. + // :!:>section_1a + const client = new AptosClient(NODE_URL); + const faucetClient = new FaucetClient(NODE_URL, FAUCET_URL); // <:!:section_1a + + // Create client for working with the token module. + // :!:>section_1b + const tokenClient = new TokenClient(client); // <:!:section_1b + + // Create a coin client for checking account balances. + const coinClient = new CoinClient(client); + + // Create accounts. + // :!:>section_2 + const alice = new AptosAccount(); + const bob = new AptosAccount(); // <:!:section_2 + + // Print out account addresses. + console.log("=== Addresses ==="); + console.log(`Alice: ${alice.address()}`); + console.log(`Alice: ${bob.address()}`); + console.log(""); + + // Fund accounts. + // :!:>section_3 + await faucetClient.fundAccount(alice.address(), 20_000); + await faucetClient.fundAccount(bob.address(), 20_000); // <:!:section_3 + + console.log("=== Initial Coin Balances ==="); + console.log(`Alice: ${await coinClient.checkBalance(alice)}`); + console.log(`Bob: ${await coinClient.checkBalance(bob)}`); + console.log(""); + + console.log("=== Creating Collection and Token ==="); + + const collectionName = "Alice's"; + const tokenName = "Alice's first token"; + const tokenPropertyVersion = 0; + + const tokenId = { + token_data_id: { + creator: alice.address().hex(), + collection: collectionName, + name: tokenName, + }, + property_version: `${tokenPropertyVersion}`, + }; + + // Create the collection. + // :!:>section_4 + const txnHash1 = await tokenClient.createCollection( + alice, + collectionName, + "Alice's simple collection", + "https://alice.com", + ); // <:!:section_4 + await client.waitForTransaction(txnHash1, { checkSuccess: true }); + + // Create a token in that collection. + // :!:>section_5 + const txnHash2 = await tokenClient.createToken( + alice, + collectionName, + tokenName, + "Alice's simple token", + 1, + "https://aptos.dev/img/nyan.jpeg", + ); // <:!:section_5 + await client.waitForTransaction(txnHash2, { checkSuccess: true }); + + // Print the collection data. + // :!:>section_6 + const collectionData = await tokenClient.getCollectionData(alice.address(), collectionName); + console.log(`Alice's collection: ${JSON.stringify(collectionData, null, 4)}`); // <:!:section_6 + + // Get the token balance. + // :!:>section_7 + const aliceBalance1 = await tokenClient.getToken( + alice.address(), + collectionName, + tokenName, + `${tokenPropertyVersion}`, + ); + console.log(`Alice's token balance: ${aliceBalance1["amount"]}`); // <:!:section_7 + + // Get the token data. + // :!:>section_8 + const tokenData = await tokenClient.getTokenData(alice.address(), collectionName, tokenName); + console.log(`Alice's token data: ${JSON.stringify(tokenData, null, 4)}`); // <:!:section_8 + + // Alice offers one token to Bob. + console.log("\n=== Transferring the token to Bob ==="); + // :!:>section_9 + const txnHash3 = await tokenClient.offerToken( + alice, + bob.address(), + alice.address(), + collectionName, + tokenName, + 1, + tokenPropertyVersion, + ); // <:!:section_9 + await client.waitForTransaction(txnHash3, { checkSuccess: true }); + + // Bob claims the token Alice offered him. + // :!:>section_10 + const txnHash4 = await tokenClient.claimToken( + bob, + alice.address(), + alice.address(), + collectionName, + tokenName, + tokenPropertyVersion, + ); // <:!:section_10 + await client.waitForTransaction(txnHash4, { checkSuccess: true }); + + // Print their balances. + const aliceBalance2 = await tokenClient.getToken( + alice.address(), + collectionName, + tokenName, + `${tokenPropertyVersion}`, + ); + const bobBalance2 = await tokenClient.getTokenForAccount(bob.address(), tokenId); + console.log(`Alice's token balance: ${aliceBalance2["amount"]}`); + console.log(`Bob's token balance: ${bobBalance2["amount"]}`); + + console.log("\n=== Transferring the token back to Alice using MultiAgent ==="); + // :!:>section_11 + let txnHash5 = await tokenClient.directTransferToken( + bob, + alice, + alice.address(), + collectionName, + tokenName, + 1, + tokenPropertyVersion, + ); // <:!:section_11 + await client.waitForTransaction(txnHash5, { checkSuccess: true }); + + // Print out their balances one last time. + const aliceBalance3 = await tokenClient.getToken( + alice.address(), + collectionName, + tokenName, + `${tokenPropertyVersion}`, + ); + const bobBalance3 = await tokenClient.getTokenForAccount(bob.address(), tokenId); + console.log(`Alice's token balance: ${aliceBalance3["amount"]}`); + console.log(`Bob's token balance: ${bobBalance3["amount"]}`); +})(); diff --git a/ecosystem/typescript/sdk/examples/typescript/transfer_coin.ts b/ecosystem/typescript/sdk/examples/typescript/transfer_coin.ts index 20df57d5d5cb8..72059ec881caa 100644 --- a/ecosystem/typescript/sdk/examples/typescript/transfer_coin.ts +++ b/ecosystem/typescript/sdk/examples/typescript/transfer_coin.ts @@ -7,7 +7,7 @@ import dotenv from "dotenv"; dotenv.config(); import { AptosClient, AptosAccount, CoinClient, FaucetClient } from "aptos"; -import { aptosCoinStore, NODE_URL, FAUCET_URL } from "./common"; +import { NODE_URL, FAUCET_URL } from "./common"; (async () => { // Create API and faucet clients. diff --git a/ecosystem/typescript/sdk/src/abis.ts b/ecosystem/typescript/sdk/src/abis.ts index b9f7e5f64f36d..479bcb9687523 100644 --- a/ecosystem/typescript/sdk/src/abis.ts +++ b/ecosystem/typescript/sdk/src/abis.ts @@ -3,11 +3,15 @@ /* eslint-disable max-len */ +// https://aptos.dev/sdks/transactions-with-ts-sdk + export const TOKEN_ABIS = [ // aptos-token/build/AptosToken/abis/token/create_collection_script.abi "01186372656174655F636F6C6C656374696F6E5F736372697074000000000000000000000000000000000000000000000000000000000000000305746F6B656E3020637265617465206120656D70747920746F6B656E20636F6C6C656374696F6E207769746820706172616D65746572730005046E616D6507000000000000000000000000000000000000000000000000000000000000000106737472696E6706537472696E67000B6465736372697074696F6E07000000000000000000000000000000000000000000000000000000000000000106737472696E6706537472696E67000375726907000000000000000000000000000000000000000000000000000000000000000106737472696E6706537472696E6700076D6178696D756D020E6D75746174655F73657474696E670600", // aptos-token/build/AptosToken/abis/token/create_token_script.abi "01136372656174655F746F6B656E5F736372697074000000000000000000000000000000000000000000000000000000000000000305746F6B656E1D2063726561746520746F6B656E20776974682072617720696E70757473000D0A636F6C6C656374696F6E07000000000000000000000000000000000000000000000000000000000000000106737472696E6706537472696E6700046E616D6507000000000000000000000000000000000000000000000000000000000000000106737472696E6706537472696E67000B6465736372697074696F6E07000000000000000000000000000000000000000000000000000000000000000106737472696E6706537472696E67000762616C616E636502076D6178696D756D020375726907000000000000000000000000000000000000000000000000000000000000000106737472696E6706537472696E670015726F79616C74795F70617965655F61646472657373041A726F79616C74795F706F696E74735F64656E6F6D696E61746F720218726F79616C74795F706F696E74735F6E756D657261746F72020E6D75746174655F73657474696E6706000D70726F70657274795F6B6579730607000000000000000000000000000000000000000000000000000000000000000106737472696E6706537472696E67000F70726F70657274795F76616C7565730606010E70726F70657274795F74797065730607000000000000000000000000000000000000000000000000000000000000000106737472696E6706537472696E6700", + // aptos-token/build/AptosToken/abis/token/direct_transfer_script.abi + "01166469726563745f7472616e736665725f736372697074000000000000000000000000000000000000000000000000000000000000000305746f6b656e0000051063726561746f72735f61646472657373040a636f6c6c656374696f6e07000000000000000000000000000000000000000000000000000000000000000106737472696e6706537472696e6700046e616d6507000000000000000000000000000000000000000000000000000000000000000106737472696e6706537472696e67001070726f70657274795f76657273696f6e0206616d6f756e7402", // aptos-token/build/AptosToken/abis/token_transfers/offer_script.abi "010C6F666665725F73637269707400000000000000000000000000000000000000000000000000000000000000030F746F6B656E5F7472616E7366657273000006087265636569766572040763726561746F72040A636F6C6C656374696F6E07000000000000000000000000000000000000000000000000000000000000000106737472696E6706537472696E6700046E616D6507000000000000000000000000000000000000000000000000000000000000000106737472696E6706537472696E67001070726F70657274795F76657273696F6E0206616D6F756E7402", // aptos-token/build/AptosToken/abis/token_transfers/claim_script.abi diff --git a/ecosystem/typescript/sdk/src/aptos_client.test.ts b/ecosystem/typescript/sdk/src/aptos_client.test.ts index 3468e70389680..b5c63c40b9da5 100644 --- a/ecosystem/typescript/sdk/src/aptos_client.test.ts +++ b/ecosystem/typescript/sdk/src/aptos_client.test.ts @@ -399,58 +399,17 @@ test( let aliceBalance = await tokenClient.getTokenForAccount(alice.address().hex(), tokenId); expect(aliceBalance.amount).toBe("1"); - const entryFunctionPayload = new TxnBuilderTypes.TransactionPayloadEntryFunction( - TxnBuilderTypes.EntryFunction.natural( - "0x3::token", - "direct_transfer_script", - [], - [ - BCS.bcsToBytes(TxnBuilderTypes.AccountAddress.fromHex(alice.address())), - BCS.bcsSerializeStr(collectionName), - BCS.bcsSerializeStr(tokenName), - BCS.bcsSerializeUint64(propertyVersion), - BCS.bcsSerializeUint64(1), - ], - ), + const txnHash = await tokenClient.directTransferToken( + alice, + bob, + alice.address(), + collectionName, + tokenName, + 1, + propertyVersion, ); - const rawTxn = await client.generateRawTransaction(alice.address(), entryFunctionPayload); - const multiAgentTxn = new TxnBuilderTypes.MultiAgentRawTransaction(rawTxn, [ - TxnBuilderTypes.AccountAddress.fromHex(bob.address()), - ]); - - const aliceSignature = new TxnBuilderTypes.Ed25519Signature( - alice.signBuffer(TransactionBuilder.getSigningMessage(multiAgentTxn)).toUint8Array(), - ); - - const aliceAuthenticator = new TxnBuilderTypes.AccountAuthenticatorEd25519( - new TxnBuilderTypes.Ed25519PublicKey(alice.signingKey.publicKey), - aliceSignature, - ); - - const bobSignature = new TxnBuilderTypes.Ed25519Signature( - bob.signBuffer(TransactionBuilder.getSigningMessage(multiAgentTxn)).toUint8Array(), - ); - - const bobAuthenticator = new TxnBuilderTypes.AccountAuthenticatorEd25519( - new TxnBuilderTypes.Ed25519PublicKey(bob.signingKey.publicKey), - bobSignature, - ); - - const multiAgentAuthenticator = new TxnBuilderTypes.TransactionAuthenticatorMultiAgent( - aliceAuthenticator, // sender authenticator - [TxnBuilderTypes.AccountAddress.fromHex(bob.address())], // secondary signer addresses - [bobAuthenticator], // secondary signer authenticators - ); - - const bcsTxn = BCS.bcsToBytes(new TxnBuilderTypes.SignedTransaction(rawTxn, multiAgentAuthenticator)); - - const transactionRes = await client.submitSignedBCSTransaction(bcsTxn); - - await client.waitForTransaction(transactionRes.hash); - - const transaction = await client.getTransactionByHash(transactionRes.hash); - expect((transaction as any)?.success).toBe(true); + await client.waitForTransaction(txnHash, { checkSuccess: true }); aliceBalance = await tokenClient.getTokenForAccount(alice.address().hex(), tokenId); expect(aliceBalance.amount).toBe("0"); diff --git a/ecosystem/typescript/sdk/src/token_client.ts b/ecosystem/typescript/sdk/src/token_client.ts index 7632c046d8275..d926e37297d7f 100644 --- a/ecosystem/typescript/sdk/src/token_client.ts +++ b/ecosystem/typescript/sdk/src/token_client.ts @@ -6,7 +6,7 @@ import { AptosClient } from "./aptos_client"; import * as TokenTypes from "./token_types"; import * as Gen from "./generated/index"; import { HexString, MaybeHexString } from "./hex_string"; -import { BCS, TransactionBuilderABI } from "./transaction_builder"; +import { BCS, TransactionBuilder, TransactionBuilderABI, TxnBuilderTypes } from "./transaction_builder"; import { MAX_U64_BIG_INT } from "./transaction_builder/bcs/consts"; import { TOKEN_ABIS } from "./abis"; @@ -20,6 +20,7 @@ export class TokenClient { /** * Creates new TokenClient instance + * * @param aptosClient AptosClient instance */ constructor(aptosClient: AptosClient) { @@ -29,6 +30,7 @@ export class TokenClient { /** * Creates a new NFT collection within the specified account + * * @param account AptosAccount where collection will be created * @param name Collection name * @param description Collection description @@ -36,6 +38,7 @@ export class TokenClient { * @param maxAmount Maximum number of `token_data` allowed within this collection * @returns The hash of the transaction submitted to the API */ + // :!:>createCollection async createCollection( account: AptosAccount, name: string, @@ -43,6 +46,7 @@ export class TokenClient { uri: string, maxAmount: BCS.AnyNumber = MAX_U64_BIG_INT, ): Promise { + // <:!:createCollection const payload = this.transactionBuilder.buildTransactionPayload( "0x3::token::create_collection_script", [], @@ -54,6 +58,7 @@ export class TokenClient { /** * Creates a new NFT within the specified account + * * @param account AptosAccount where token will be created * @param collectionName Name of collection, that token belongs to * @param name Token name @@ -69,6 +74,7 @@ export class TokenClient { * @param property_types the type of property values * @returns The hash of the transaction submitted to the API */ + // :!:>createToken async createToken( account: AptosAccount, collectionName: string, @@ -84,6 +90,7 @@ export class TokenClient { property_values: Array = [], property_types: Array = [], ): Promise { + // <:!:createToken const payload = this.transactionBuilder.buildTransactionPayload( "0x3::token::create_token_script", [], @@ -109,6 +116,7 @@ export class TokenClient { /** * Transfers specified amount of tokens from account to receiver + * * @param account AptosAccount where token from which tokens will be transfered * @param receiver Hex-encoded 32 byte Aptos account address to which tokens will be transfered * @param creator Hex-encoded 32 byte Aptos account address to which created tokens @@ -138,6 +146,7 @@ export class TokenClient { /** * Claims a token on specified account + * * @param account AptosAccount which will claim token * @param sender Hex-encoded 32 byte Aptos account address which holds a token * @param creator Hex-encoded 32 byte Aptos account address which created a token @@ -165,6 +174,7 @@ export class TokenClient { /** * Removes a token from pending claims list + * * @param account AptosAccount which will remove token from pending list * @param receiver Hex-encoded 32 byte Aptos account address which had to claim token * @param creator Hex-encoded 32 byte Aptos account address which created a token @@ -190,6 +200,70 @@ export class TokenClient { return this.aptosClient.generateSignSubmitTransaction(account, payload); } + /** + * Directly transfer the specified amount of tokens from account to receiver + * using a single multi signature transaction. + * + * @param account AptosAccount where token from which tokens will be transfered + * @param receiver Hex-encoded 32 byte Aptos account address to which tokens will be transfered + * @param creator Hex-encoded 32 byte Aptos account address to which created tokens + * @param collectionName Name of collection where token is stored + * @param name Token name + * @param amount Amount of tokens which will be transfered + * @param property_version the version of token PropertyMap with a default value 0. + * @returns The hash of the transaction submitted to the API + */ + async directTransferToken( + sender: AptosAccount, + receiver: AptosAccount, + creator: MaybeHexString, + collectionName: string, + name: string, + amount: number, + propertyVersion: number = 0, + ): Promise { + const payload = this.transactionBuilder.buildTransactionPayload( + "0x3::token::direct_transfer_script", + [], + [creator, collectionName, name, propertyVersion, amount], + ); + + const rawTxn = await this.aptosClient.generateRawTransaction(sender.address(), payload); + const multiAgentTxn = new TxnBuilderTypes.MultiAgentRawTransaction(rawTxn, [ + TxnBuilderTypes.AccountAddress.fromHex(receiver.address()), + ]); + + const senderSignature = new TxnBuilderTypes.Ed25519Signature( + sender.signBuffer(TransactionBuilder.getSigningMessage(multiAgentTxn)).toUint8Array(), + ); + + const senderAuthenticator = new TxnBuilderTypes.AccountAuthenticatorEd25519( + new TxnBuilderTypes.Ed25519PublicKey(sender.signingKey.publicKey), + senderSignature, + ); + + const receiverSignature = new TxnBuilderTypes.Ed25519Signature( + receiver.signBuffer(TransactionBuilder.getSigningMessage(multiAgentTxn)).toUint8Array(), + ); + + const receiverAuthenticator = new TxnBuilderTypes.AccountAuthenticatorEd25519( + new TxnBuilderTypes.Ed25519PublicKey(receiver.signingKey.publicKey), + receiverSignature, + ); + + const multiAgentAuthenticator = new TxnBuilderTypes.TransactionAuthenticatorMultiAgent( + senderAuthenticator, + [TxnBuilderTypes.AccountAddress.fromHex(receiver.address())], // Secondary signer addresses + [receiverAuthenticator], // Secondary signer authenticators + ); + + const bcsTxn = BCS.bcsToBytes(new TxnBuilderTypes.SignedTransaction(rawTxn, multiAgentAuthenticator)); + + const transactionRes = await this.aptosClient.submitSignedBCSTransaction(bcsTxn); + + return transactionRes.hash; + } + /** * Queries collection data * @param creator Hex-encoded 32 byte Aptos account address which created a collection @@ -228,6 +302,7 @@ export class TokenClient { /** * Queries token data from collection + * * @param creator Hex-encoded 32 byte Aptos account address which created a token * @param collectionName Name of collection, which holds a token * @param tokenName Token name @@ -249,18 +324,20 @@ export class TokenClient { * } * ``` */ + // :!:>getTokenData async getTokenData( creator: MaybeHexString, collectionName: string, tokenName: string, ): Promise { + const creatorHex = creator instanceof HexString ? creator.hex() : creator; const collection: { type: Gen.MoveStructTag; data: any } = await this.aptosClient.getAccountResource( - creator, + creatorHex, "0x3::token::Collections", ); const { handle } = collection.data.token_data; const tokenDataId = { - creator, + creator: creatorHex, collection: collectionName, name: tokenName, }; @@ -274,7 +351,7 @@ export class TokenClient { // We know the response will be a struct containing TokenData, hence the // implicit cast. return this.aptosClient.getTableItem(handle, getTokenTableItemRequest); - } + } // <:!:getTokenData /** * Queries token balance for the token creator @@ -297,7 +374,6 @@ export class TokenClient { } /** - * TODO: What does this mean? Is it more like getTokenBalanceInAccount? * Queries token balance for a token account * @param account Hex-encoded 32 byte Aptos account address which created a token * @param tokenId token id @@ -321,7 +397,7 @@ export class TokenClient { */ async getTokenForAccount(account: MaybeHexString, tokenId: TokenTypes.TokenId): Promise { const tokenStore: { type: Gen.MoveStructTag; data: any } = await this.aptosClient.getAccountResource( - account, + account instanceof HexString ? account.hex() : account, "0x3::token::TokenStore", ); const { handle } = tokenStore.data.tokens;