diff --git a/src/api/account.ts b/src/api/account.ts index 7adba8977..18e883b90 100644 --- a/src/api/account.ts +++ b/src/api/account.ts @@ -299,10 +299,10 @@ export class Account { } /** - * Queries the count of an account's coins + * Queries the count of an account's coins aggregated * * @param accountAddress The account address we want to get the total count for - * @returns An object { count : number } + * @returns An object { count : number } where `number` is the aggregated count of all account's coin */ async getAccountCoinsCount(args: { accountAddress: HexInput }): Promise { const count = getAccountCoinsCount({ aptosConfig: this.config, ...args }); diff --git a/src/api/aptos.ts b/src/api/aptos.ts index 810d47af5..475a61dec 100644 --- a/src/api/aptos.ts +++ b/src/api/aptos.ts @@ -3,6 +3,7 @@ import { Account } from "./account"; import { AptosConfig } from "./aptos_config"; +import { Coin } from "./coin"; import { Faucet } from "./faucet"; import { General } from "./general"; import { Transaction } from "./transaction"; @@ -13,6 +14,8 @@ export class Aptos { readonly account: Account; + readonly coin: Coin; + readonly faucet: Faucet; readonly general: General; @@ -41,6 +44,7 @@ export class Aptos { constructor(settings?: AptosConfig) { this.config = new AptosConfig(settings); this.account = new Account(this.config); + this.coin = new Coin(this.config); this.faucet = new Faucet(this.config); this.general = new General(this.config); this.transaction = new Transaction(this.config); @@ -48,7 +52,7 @@ export class Aptos { } } -export interface Aptos extends Account, Faucet, General, Transaction, TransactionSubmission {} +export interface Aptos extends Account, Coin, Faucet, General, Transaction, TransactionSubmission {} /** In TypeScript, we can’t inherit or extend from more than one class, @@ -72,6 +76,7 @@ function applyMixin(targetClass: any, baseClass: any, baseClassProp: string) { } applyMixin(Aptos, Account, "account"); +applyMixin(Aptos, Coin, "coin"); applyMixin(Aptos, Faucet, "faucet"); applyMixin(Aptos, General, "general"); applyMixin(Aptos, Transaction, "transaction"); diff --git a/src/api/coin.ts b/src/api/coin.ts new file mode 100644 index 000000000..a5e717250 --- /dev/null +++ b/src/api/coin.ts @@ -0,0 +1,40 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import { Account } from "../core"; +import { transferCoinTransaction } from "../internal/coin"; +import { SingleSignerTransaction, GenerateTransactionOptions } from "../transactions/types"; +import { AnyNumber, HexInput, MoveResourceType } from "../types"; +import { AptosConfig } from "./aptos_config"; + +/** + * A class to handle all `Coin` operations + */ +export class Coin { + readonly config: AptosConfig; + + constructor(config: AptosConfig) { + this.config = config; + } + + /** + * Generate a transfer coin transaction that can be simulated and/or signed and submitted + * + * @param args.sender The sender account + * @param args.recipient The recipient address + * @param args.amount The amount to transfer + * @param args.coinType optional. The coin struct type to transfer. Default to 0x1::aptos_coin::AptosCoin + * + * @returns SingleSignerTransaction + */ + async transferCoinTransaction(args: { + sender: Account; + recipient: HexInput; + amount: AnyNumber; + coinType?: MoveResourceType; + options?: GenerateTransactionOptions; + }): Promise { + const response = await transferCoinTransaction({ aptosConfig: this.config, ...args }); + return response; + } +} diff --git a/src/api/transaction_submission.ts b/src/api/transaction_submission.ts index c5f7b4682..755fa9971 100644 --- a/src/api/transaction_submission.ts +++ b/src/api/transaction_submission.ts @@ -141,4 +141,33 @@ export class TransactionSubmission { const data = await submitTransaction({ aptosConfig: this.config, ...args }); return data; } + + /** + * Sign and submit a single signer transaction to chain + * + * @param args.signer The signer account to sign the transaction + * @param args.transaction A raw transaction type (note that it holds the raw transaction as a bcs serialized data) + * ``` + * { + * rawTransaction: Uint8Array, + * secondarySignerAddresses? : Array, + * feePayerAddress?: AccountAddress + * } + * ``` + * + * @return PendingTransactionResponse + */ + async signAndSubmitTransaction(args: { + signer: Account; + transaction: AnyRawTransaction; + }): Promise { + const { signer, transaction } = args; + const authenticator = signTransaction({ signer, transaction }); + const response = await submitTransaction({ + aptosConfig: this.config, + transaction, + senderAuthenticator: authenticator, + }); + return response; + } } diff --git a/src/internal/coin.ts b/src/internal/coin.ts new file mode 100644 index 000000000..1b030dd40 --- /dev/null +++ b/src/internal/coin.ts @@ -0,0 +1,32 @@ +import { AptosConfig } from "../api/aptos_config"; +import { U64 } from "../bcs/serializable/move-primitives"; +import { Account, AccountAddress } from "../core"; +import { GenerateTransactionOptions, SingleSignerTransaction } from "../transactions/types"; +import { StructTag, TypeTagStruct } from "../transactions/typeTag/typeTag"; +import { HexInput, AnyNumber, MoveResourceType } from "../types"; +import { APTOS_COIN } from "../utils/const"; +import { generateTransaction } from "./transaction_submission"; + +export async function transferCoinTransaction(args: { + aptosConfig: AptosConfig; + sender: Account; + recipient: HexInput; + amount: AnyNumber; + coinType?: MoveResourceType; + options?: GenerateTransactionOptions; +}): Promise { + const { aptosConfig, sender, recipient, amount, coinType, options } = args; + const coinStructType = coinType ?? APTOS_COIN; + const transaction = await generateTransaction({ + aptosConfig, + sender: sender.accountAddress.toString(), + data: { + function: "0x1::aptos_account::transfer_coins", + type_arguments: [new TypeTagStruct(StructTag.fromString(coinStructType))], + arguments: [AccountAddress.fromHexInput({ input: recipient }), new U64(amount)], + }, + options, + }); + + return transaction as SingleSignerTransaction; +} diff --git a/tests/e2e/api/coin.test.ts b/tests/e2e/api/coin.test.ts new file mode 100644 index 000000000..0073b2d49 --- /dev/null +++ b/tests/e2e/api/coin.test.ts @@ -0,0 +1,76 @@ +import { AptosConfig, Network, Aptos, Account, Deserializer } from "../../../src"; +import { waitForTransaction } from "../../../src/internal/transaction"; +import { RawTransaction, TransactionPayloadEntryFunction } from "../../../src/transactions/instances"; +import { TypeTagStruct } from "../../../src/transactions/typeTag/typeTag"; +import { SigningScheme } from "../../../src/types"; + +describe("coin", () => { + test("it generates a transfer coin transaction with AptosCoin coin type", async () => { + const config = new AptosConfig({ network: Network.DEVNET }); + const aptos = new Aptos(config); + const sender = Account.generate({ scheme: SigningScheme.Ed25519 }); + const recipient = Account.generate({ scheme: SigningScheme.Ed25519 }); + await aptos.fundAccount({ accountAddress: sender.accountAddress.toString(), amount: 100000000 }); + + const transaction = await aptos.transferCoinTransaction({ + sender, + recipient: recipient.accountAddress.toString(), + amount: 10, + }); + + const txnDeserializer = new Deserializer(transaction.rawTransaction); + const rawTransaction = RawTransaction.deserialize(txnDeserializer); + const typeArgs = (rawTransaction.payload as TransactionPayloadEntryFunction).entryFunction.type_args; + expect((typeArgs[0] as TypeTagStruct).value.address.toString()).toBe("0x1"); + expect((typeArgs[0] as TypeTagStruct).value.module_name.identifier).toBe("aptos_coin"); + expect((typeArgs[0] as TypeTagStruct).value.name.identifier).toBe("AptosCoin"); + }); + + test("it generates a transfer coin transaction with a custom coin type", async () => { + const config = new AptosConfig({ network: Network.DEVNET }); + const aptos = new Aptos(config); + const sender = Account.generate({ scheme: SigningScheme.Ed25519 }); + const recipient = Account.generate({ scheme: SigningScheme.Ed25519 }); + await aptos.fundAccount({ accountAddress: sender.accountAddress.toString(), amount: 100000000 }); + + const transaction = await aptos.transferCoinTransaction({ + sender, + recipient: recipient.accountAddress.toString(), + amount: 10, + coinType: "0x1::my_coin::type", + }); + + const txnDeserializer = new Deserializer(transaction.rawTransaction); + const rawTransaction = RawTransaction.deserialize(txnDeserializer); + const typeArgs = (rawTransaction.payload as TransactionPayloadEntryFunction).entryFunction.type_args; + expect((typeArgs[0] as TypeTagStruct).value.address.toString()).toBe("0x1"); + expect((typeArgs[0] as TypeTagStruct).value.module_name.identifier).toBe("my_coin"); + expect((typeArgs[0] as TypeTagStruct).value.name.identifier).toBe("type"); + }); + + test("it transfers APT coin aomunt from sender to recipient", async () => { + const config = new AptosConfig({ network: Network.DEVNET }); + const aptos = new Aptos(config); + const sender = Account.generate({ scheme: SigningScheme.Ed25519 }); + const recipient = Account.generate({ scheme: SigningScheme.Ed25519 }); + + await aptos.fundAccount({ accountAddress: sender.accountAddress.toString(), amount: 100000000 }); + const senderCoinsBefore = await aptos.getAccountCoinsData({ accountAddress: sender.accountAddress.toString() }); + + const transaction = await aptos.transferCoinTransaction({ + sender, + recipient: recipient.accountAddress.toString(), + amount: 10, + }); + const response = await aptos.signAndSubmitTransaction({ signer: sender, transaction }); + + await waitForTransaction({ aptosConfig: config, txnHash: response.hash }); + + const recipientCoins = await aptos.getAccountCoinsData({ accountAddress: recipient.accountAddress.toString() }); + const senderCoinsAfter = await aptos.getAccountCoinsData({ accountAddress: sender.accountAddress.toString() }); + + expect(recipientCoins[0].amount).toBe(10); + expect(recipientCoins[0].asset_type).toBe("0x1::aptos_coin::AptosCoin"); + expect(senderCoinsAfter[0].amount).toBeLessThan(senderCoinsBefore[0].amount); + }); +});