From fea5e18cb37916f40e3372a56283f992a096675c Mon Sep 17 00:00:00 2001 From: maayan Date: Tue, 9 Apr 2024 21:00:58 +0300 Subject: [PATCH 1/3] support general proof challenege --- src/api/general.ts | 11 ++++- src/api/transaction.ts | 11 ++++- src/internal/general.ts | 37 +++++++++++++++ src/internal/transactionSubmission.ts | 12 ++++- .../instances/rotationProofChallenge.ts | 15 +++++++ .../transactionBuilder/remoteAbi.ts | 40 +++++++++++++++++ tests/e2e/transaction/signTransaction.test.ts | 45 +++++++++++++++++++ 7 files changed, 167 insertions(+), 4 deletions(-) diff --git a/src/api/general.ts b/src/api/general.ts index b870beca5..6a6d41bf6 100644 --- a/src/api/general.ts +++ b/src/api/general.ts @@ -3,6 +3,7 @@ import { AptosConfig } from "./aptosConfig"; import { + createProofChallenge, getBlockByHeight, getBlockByVersion, getChainTopUserTransactions, @@ -21,11 +22,12 @@ import { GraphqlQuery, LedgerInfo, LedgerVersionArg, + MoveFunctionId, MoveValue, TableItemRequest, } from "../types"; import { ProcessorType } from "../utils/const"; -import { InputViewFunctionData } from "../transactions"; +import { InputViewFunctionData, ProofChallenge } from "../transactions"; /** * A class to query all `General` Aptos related queries @@ -224,4 +226,11 @@ export class General { async getProcessorStatus(processorType: ProcessorType): Promise { return getProcessorStatus({ aptosConfig: this.config, processorType }); } + + async createProofChallenge(args: { struct: MoveFunctionId; data: Array }): Promise { + return createProofChallenge({ + config: this.config, + ...args, + }); + } } diff --git a/src/api/transaction.ts b/src/api/transaction.ts index 612bfd952..22484db8a 100644 --- a/src/api/transaction.ts +++ b/src/api/transaction.ts @@ -25,6 +25,7 @@ import { publicPackageTransaction, rotateAuthKey, signAndSubmitTransaction, + signProofChallenge, signTransaction, } from "../internal/transactionSubmission"; import { @@ -32,8 +33,9 @@ import { AnyRawTransaction, InputGenerateTransactionOptions, InputGenerateTransactionPayloadData, + ProofChallenge, } from "../transactions"; -import { AccountAddressInput, Account, PrivateKey } from "../core"; +import { AccountAddressInput, Account, PrivateKey, Signature } from "../core"; import { Build } from "./transactionSubmission/build"; import { Simulate } from "./transactionSubmission/simulate"; import { Submit } from "./transactionSubmission/submit"; @@ -306,6 +308,13 @@ export class Transaction { }); } + // eslint-disable-next-line class-methods-use-this + signProofChallenge(args: { challenge: ProofChallenge; signer: Account }): Signature { + return signProofChallenge({ + ...args, + }); + } + // TRANSACTION SUBMISSION // /** diff --git a/src/internal/general.ts b/src/internal/general.ts index 673c4a377..2f3de87c9 100644 --- a/src/internal/general.ts +++ b/src/internal/general.ts @@ -9,7 +9,13 @@ */ import { AptosConfig } from "../api/aptosConfig"; +import { MoveString } from "../bcs"; import { getAptosFullNode, postAptosFullNode, postAptosIndexer } from "../client"; +import { AccountAddress } from "../core"; +import { getFunctionParts } from "../transactions/transactionBuilder/helpers"; +import { fetchStructFieldsAbi, convertArgument } from "../transactions/transactionBuilder/remoteAbi"; +import { ProofChallenge } from "../transactions/instances"; +import { EntryFunctionArgumentTypes } from "../transactions/types"; import { AnyNumber, Block, @@ -18,6 +24,7 @@ import { GraphqlQuery, LedgerInfo, LedgerVersionArg, + MoveFunctionId, TableItemRequest, } from "../types"; import { GetChainTopUserTransactionsQuery, GetProcessorStatusQuery } from "../types/generated/operations"; @@ -162,3 +169,33 @@ export async function getProcessorStatus(args: { return data.processor_status[0]; } + +export async function createProofChallenge(args: { + config: AptosConfig; + struct: MoveFunctionId; + data: Array; +}): Promise { + const { config, struct, data } = args; + const { moduleAddress, moduleName, functionName } = getFunctionParts(struct); + const structFieldsAbi = await fetchStructFieldsAbi(moduleAddress, moduleName, functionName, config); + + // Check all BCS types, and convert any non-BCS types + const functionArguments: Array = + data.map((arg, i) => convertArgument(functionName, structFieldsAbi, arg, i, structFieldsAbi.parameters)) ?? []; + + // Check that all arguments are accounted for + if (functionArguments.length !== structFieldsAbi.parameters.length) { + throw new Error( + // eslint-disable-next-line max-len + `Too few arguments for '${moduleAddress}::${moduleName}::${functionName}', expected ${structFieldsAbi.parameters.length} but got ${functionArguments.length}`, + ); + } + + const challenge = new ProofChallenge([ + AccountAddress.from(moduleAddress), + new MoveString(moduleName), + new MoveString(functionName), + ...functionArguments, + ]); + return challenge; +} diff --git a/src/internal/transactionSubmission.ts b/src/internal/transactionSubmission.ts index 91d555a76..bf7878ff4 100644 --- a/src/internal/transactionSubmission.ts +++ b/src/internal/transactionSubmission.ts @@ -10,9 +10,9 @@ import { MoveVector, U8 } from "../bcs"; import { postAptosFullNode } from "../client"; import { Account } from "../core/account"; import { AccountAddress, AccountAddressInput } from "../core/accountAddress"; -import { PrivateKey } from "../core/crypto"; +import { PrivateKey, Signature } from "../core/crypto"; import { AccountAuthenticator } from "../transactions/authenticator/account"; -import { RotationProofChallenge } from "../transactions/instances/rotationProofChallenge"; +import { ProofChallenge, RotationProofChallenge } from "../transactions/instances/rotationProofChallenge"; import { buildTransaction, generateTransactionPayload, @@ -208,6 +208,13 @@ export function signTransaction(args: { signer: Account; transaction: AnyRawTran return accountAuthenticator; } +export function signProofChallenge(args: { challenge: ProofChallenge; signer: Account }): Signature { + const { challenge, signer } = args; + const challengeHex = challenge.bcsToBytes(); + const signature = signer.sign(challengeHex); + return signature; +} + /** * Simulates a transaction before singing it. * @@ -350,6 +357,7 @@ export async function rotateAuthKey(args: { // Sign the challenge const challengeHex = challenge.bcsToBytes(); + const proofSignedByCurrentPrivateKey = fromAccount.sign(challengeHex); const proofSignedByNewPrivateKey = newAccount.sign(challengeHex); diff --git a/src/transactions/instances/rotationProofChallenge.ts b/src/transactions/instances/rotationProofChallenge.ts index dcc2e6dc2..8c899db1b 100644 --- a/src/transactions/instances/rotationProofChallenge.ts +++ b/src/transactions/instances/rotationProofChallenge.ts @@ -7,6 +7,21 @@ import { AnyNumber } from "../../types"; import { PublicKey } from "../../core/crypto"; import { MoveString, MoveVector, U64, U8 } from "../../bcs"; +export class ProofChallenge extends Serializable { + public readonly data: Serializable[]; + + constructor(data: Serializable[]) { + super(); + this.data = data; + } + + serialize(serializer: Serializer): void { + this.data.forEach((data) => { + serializer.serialize(data); + }); + } +} + /** * Representation of the challenge which is needed to sign by owner of the account * to rotate the authentication key. diff --git a/src/transactions/transactionBuilder/remoteAbi.ts b/src/transactions/transactionBuilder/remoteAbi.ts index cee854e29..83bdc4e16 100644 --- a/src/transactions/transactionBuilder/remoteAbi.ts +++ b/src/transactions/transactionBuilder/remoteAbi.ts @@ -75,6 +75,22 @@ export async function fetchFunctionAbi( return undefined; } +export async function fetchStructAbi( + moduleAddress: string, + moduleName: string, + structName: string, + aptosConfig: AptosConfig, +) { + // This fetch from the API is currently cached + const module = await getModule({ aptosConfig, accountAddress: moduleAddress, moduleName }); + + if (module.abi) { + return module.abi.structs.find((struct) => struct.name === structName); + } + + return undefined; +} + /** * Fetches the ABI for an entry function from the module * @@ -160,6 +176,30 @@ export async function fetchViewFunctionAbi( }; } +export async function fetchStructFieldsAbi( + moduleAddress: string, + moduleName: string, + structName: string, + aptosConfig: AptosConfig, +) { + const structAbi = await fetchStructAbi(moduleAddress, moduleName, structName, aptosConfig); + + // If there's no ABI, then the function is invalid + if (!structAbi) { + throw new Error(`Could not find Struct ABI for '${moduleAddress}::${moduleName}::${structName}'`); + } + + const params: TypeTag[] = []; + for (let i = 0; i < structAbi.fields.length; i += 1) { + params.push(parseTypeTag(structAbi.fields[i].type, { allowGenerics: true })); + } + + return { + typeParameters: structAbi.generic_type_params, + parameters: params, + }; +} + /** * Converts a non-BCS encoded argument into BCS encoded, if necessary * @param functionName diff --git a/tests/e2e/transaction/signTransaction.test.ts b/tests/e2e/transaction/signTransaction.test.ts index a1b7dbf0b..236a64e64 100644 --- a/tests/e2e/transaction/signTransaction.test.ts +++ b/tests/e2e/transaction/signTransaction.test.ts @@ -6,6 +6,8 @@ import { AccountAuthenticator, AccountAuthenticatorEd25519, AccountAuthenticatorSingleKey, + MoveVector, + U8, } from "../../../src"; import { longTestTimeout } from "../../unit/helper"; import { getAptosClient } from "../helper"; @@ -216,3 +218,46 @@ describe("sign transaction", () => { }); }); }); + +test.only("test", async () => { + const fromAccount = Account.generate(); + const newAccount = Account.generate(); + + await aptos.fundAccount({ accountAddress: fromAccount.accountAddress, amount: 1_000_000_000 }); + await aptos.fundAccount({ accountAddress: newAccount.accountAddress, amount: 1_000_000_000 }); + + const accountInfo = await aptos.getAccountInfo({ + accountAddress: fromAccount.accountAddress, + }); + + const challenge = await aptos.createProofChallenge({ + struct: "0x1::account::RotationProofChallenge", + data: [ + BigInt(accountInfo.sequence_number), + fromAccount.accountAddress, + accountInfo.authentication_key, + newAccount.publicKey.toUint8Array(), + ], + }); + + const proofSignedByCurrentPrivateKey = aptos.signProofChallenge({ challenge, signer: fromAccount }); + const proofSignedByNewPrivateKey = aptos.signProofChallenge({ challenge, signer: newAccount }); + + const transaction = await aptos.transaction.build.simple({ + sender: fromAccount.accountAddress, + data: { + function: "0x1::account::rotate_authentication_key", + functionArguments: [ + new U8(fromAccount.signingScheme), // from scheme + MoveVector.U8(fromAccount.publicKey.toUint8Array()), + new U8(newAccount.signingScheme), // to scheme + MoveVector.U8(newAccount.publicKey.toUint8Array()), + MoveVector.U8(proofSignedByCurrentPrivateKey.toUint8Array()), + MoveVector.U8(proofSignedByNewPrivateKey.toUint8Array()), + ], + }, + }); + + const response = await aptos.signAndSubmitTransaction({ signer: fromAccount, transaction }); + console.log("response", response); +}); From 9bd4b035f604ec866a19cc9010831a84a0929923 Mon Sep 17 00:00:00 2001 From: maayan Date: Thu, 11 Apr 2024 14:59:33 +0300 Subject: [PATCH 2/3] organize code --- src/api/aptos.ts | 6 ++ src/api/general.ts | 11 +-- src/api/proofChallenge.ts | 53 +++++++++++ src/api/transaction.ts | 11 +-- src/internal/general.ts | 37 -------- src/internal/proofChallenge.ts | 21 +++++ src/internal/transactionSubmission.ts | 12 +-- src/transactions/instances/proofChallenge.ts | 16 ++++ .../instances/rotationProofChallenge.ts | 15 --- .../transactionBuilder/transactionBuilder.ts | 43 ++++++++- tests/e2e/api/proofChallenge.test.ts | 94 +++++++++++++++++++ tests/e2e/transaction/signTransaction.test.ts | 45 --------- 12 files changed, 236 insertions(+), 128 deletions(-) create mode 100644 src/api/proofChallenge.ts create mode 100644 src/internal/proofChallenge.ts create mode 100644 src/transactions/instances/proofChallenge.ts create mode 100644 tests/e2e/api/proofChallenge.test.ts diff --git a/src/api/aptos.ts b/src/api/aptos.ts index 95714f303..2d891aaec 100644 --- a/src/api/aptos.ts +++ b/src/api/aptos.ts @@ -12,6 +12,7 @@ import { General } from "./general"; import { ANS } from "./ans"; import { Staking } from "./staking"; import { Transaction } from "./transaction"; +import { ProofChallenge } from "./proofChallenge"; /** * This class is the main entry point into Aptos's @@ -47,6 +48,8 @@ export class Aptos { readonly transaction: Transaction; + readonly proofChallenge: ProofChallenge; + constructor(settings?: AptosConfig) { this.config = new AptosConfig(settings); this.account = new Account(this.config); @@ -59,6 +62,7 @@ export class Aptos { this.general = new General(this.config); this.staking = new Staking(this.config); this.transaction = new Transaction(this.config); + this.proofChallenge = new ProofChallenge(this.config); } } @@ -74,6 +78,7 @@ export interface Aptos FungibleAsset, General, Staking, + ProofChallenge, Omit {} /** @@ -107,3 +112,4 @@ applyMixin(Aptos, FungibleAsset, "fungibleAsset"); applyMixin(Aptos, General, "general"); applyMixin(Aptos, Staking, "staking"); applyMixin(Aptos, Transaction, "transaction"); +applyMixin(Aptos, ProofChallenge, "proofChallenge"); diff --git a/src/api/general.ts b/src/api/general.ts index 6a6d41bf6..b870beca5 100644 --- a/src/api/general.ts +++ b/src/api/general.ts @@ -3,7 +3,6 @@ import { AptosConfig } from "./aptosConfig"; import { - createProofChallenge, getBlockByHeight, getBlockByVersion, getChainTopUserTransactions, @@ -22,12 +21,11 @@ import { GraphqlQuery, LedgerInfo, LedgerVersionArg, - MoveFunctionId, MoveValue, TableItemRequest, } from "../types"; import { ProcessorType } from "../utils/const"; -import { InputViewFunctionData, ProofChallenge } from "../transactions"; +import { InputViewFunctionData } from "../transactions"; /** * A class to query all `General` Aptos related queries @@ -226,11 +224,4 @@ export class General { async getProcessorStatus(processorType: ProcessorType): Promise { return getProcessorStatus({ aptosConfig: this.config, processorType }); } - - async createProofChallenge(args: { struct: MoveFunctionId; data: Array }): Promise { - return createProofChallenge({ - config: this.config, - ...args, - }); - } } diff --git a/src/api/proofChallenge.ts b/src/api/proofChallenge.ts new file mode 100644 index 000000000..b6d83bda2 --- /dev/null +++ b/src/api/proofChallenge.ts @@ -0,0 +1,53 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import { Account, Signature } from "../core"; +import { createProofChallenge, signProofChallenge } from "../internal/proofChallenge"; +import { MoveFunctionId } from "../types"; +import { AptosConfig } from "./aptosConfig"; +import { ProofChallenge as ProofChallengeInstance } from "../transactions/instances/proofChallenge"; +import { EntryFunctionArgumentTypes, SimpleEntryFunctionArgumentTypes } from "../transactions/types"; + +/** + * A class for all `ProofChallenge` Aptos related operations + */ +export class ProofChallenge { + readonly config: AptosConfig; + + constructor(config: AptosConfig) { + this.config = config; + } + + /** + * Creates a generic proof challenge + * + * @param args.struct The struct address of the challenge + * @param args.data The struct arguments + * + * @returns ProofChallenge + */ + async createProofChallenge(args: { + struct: MoveFunctionId; + data: Array; + }): Promise { + return createProofChallenge({ + config: this.config, + ...args, + }); + } + + /** + * Signs a generic proof challenge + * + * @param args.challenge The generated challenge + * @param args.signer The signer account + * + * @returns Signature + */ + // eslint-disable-next-line class-methods-use-this + signProofChallenge(args: { challenge: ProofChallengeInstance; signer: Account }): Signature { + return signProofChallenge({ + ...args, + }); + } +} diff --git a/src/api/transaction.ts b/src/api/transaction.ts index 22484db8a..612bfd952 100644 --- a/src/api/transaction.ts +++ b/src/api/transaction.ts @@ -25,7 +25,6 @@ import { publicPackageTransaction, rotateAuthKey, signAndSubmitTransaction, - signProofChallenge, signTransaction, } from "../internal/transactionSubmission"; import { @@ -33,9 +32,8 @@ import { AnyRawTransaction, InputGenerateTransactionOptions, InputGenerateTransactionPayloadData, - ProofChallenge, } from "../transactions"; -import { AccountAddressInput, Account, PrivateKey, Signature } from "../core"; +import { AccountAddressInput, Account, PrivateKey } from "../core"; import { Build } from "./transactionSubmission/build"; import { Simulate } from "./transactionSubmission/simulate"; import { Submit } from "./transactionSubmission/submit"; @@ -308,13 +306,6 @@ export class Transaction { }); } - // eslint-disable-next-line class-methods-use-this - signProofChallenge(args: { challenge: ProofChallenge; signer: Account }): Signature { - return signProofChallenge({ - ...args, - }); - } - // TRANSACTION SUBMISSION // /** diff --git a/src/internal/general.ts b/src/internal/general.ts index 2f3de87c9..673c4a377 100644 --- a/src/internal/general.ts +++ b/src/internal/general.ts @@ -9,13 +9,7 @@ */ import { AptosConfig } from "../api/aptosConfig"; -import { MoveString } from "../bcs"; import { getAptosFullNode, postAptosFullNode, postAptosIndexer } from "../client"; -import { AccountAddress } from "../core"; -import { getFunctionParts } from "../transactions/transactionBuilder/helpers"; -import { fetchStructFieldsAbi, convertArgument } from "../transactions/transactionBuilder/remoteAbi"; -import { ProofChallenge } from "../transactions/instances"; -import { EntryFunctionArgumentTypes } from "../transactions/types"; import { AnyNumber, Block, @@ -24,7 +18,6 @@ import { GraphqlQuery, LedgerInfo, LedgerVersionArg, - MoveFunctionId, TableItemRequest, } from "../types"; import { GetChainTopUserTransactionsQuery, GetProcessorStatusQuery } from "../types/generated/operations"; @@ -169,33 +162,3 @@ export async function getProcessorStatus(args: { return data.processor_status[0]; } - -export async function createProofChallenge(args: { - config: AptosConfig; - struct: MoveFunctionId; - data: Array; -}): Promise { - const { config, struct, data } = args; - const { moduleAddress, moduleName, functionName } = getFunctionParts(struct); - const structFieldsAbi = await fetchStructFieldsAbi(moduleAddress, moduleName, functionName, config); - - // Check all BCS types, and convert any non-BCS types - const functionArguments: Array = - data.map((arg, i) => convertArgument(functionName, structFieldsAbi, arg, i, structFieldsAbi.parameters)) ?? []; - - // Check that all arguments are accounted for - if (functionArguments.length !== structFieldsAbi.parameters.length) { - throw new Error( - // eslint-disable-next-line max-len - `Too few arguments for '${moduleAddress}::${moduleName}::${functionName}', expected ${structFieldsAbi.parameters.length} but got ${functionArguments.length}`, - ); - } - - const challenge = new ProofChallenge([ - AccountAddress.from(moduleAddress), - new MoveString(moduleName), - new MoveString(functionName), - ...functionArguments, - ]); - return challenge; -} diff --git a/src/internal/proofChallenge.ts b/src/internal/proofChallenge.ts new file mode 100644 index 000000000..b21d3399c --- /dev/null +++ b/src/internal/proofChallenge.ts @@ -0,0 +1,21 @@ +import { AptosConfig } from "../api/aptosConfig"; +import { Account, Signature } from "../core"; +import { EntryFunctionArgumentTypes, SimpleEntryFunctionArgumentTypes, generateProofChallenge } from "../transactions"; +import { ProofChallenge } from "../transactions/instances/proofChallenge"; +import { MoveFunctionId } from "../types"; + +export async function createProofChallenge(args: { + config: AptosConfig; + struct: MoveFunctionId; + data: Array; +}): Promise { + const challenge = generateProofChallenge({ ...args }); + return challenge; +} + +export function signProofChallenge(args: { challenge: ProofChallenge; signer: Account }): Signature { + const { challenge, signer } = args; + const challengeHex = challenge.bcsToBytes(); + const signature = signer.sign(challengeHex); + return signature; +} diff --git a/src/internal/transactionSubmission.ts b/src/internal/transactionSubmission.ts index bf7878ff4..91d555a76 100644 --- a/src/internal/transactionSubmission.ts +++ b/src/internal/transactionSubmission.ts @@ -10,9 +10,9 @@ import { MoveVector, U8 } from "../bcs"; import { postAptosFullNode } from "../client"; import { Account } from "../core/account"; import { AccountAddress, AccountAddressInput } from "../core/accountAddress"; -import { PrivateKey, Signature } from "../core/crypto"; +import { PrivateKey } from "../core/crypto"; import { AccountAuthenticator } from "../transactions/authenticator/account"; -import { ProofChallenge, RotationProofChallenge } from "../transactions/instances/rotationProofChallenge"; +import { RotationProofChallenge } from "../transactions/instances/rotationProofChallenge"; import { buildTransaction, generateTransactionPayload, @@ -208,13 +208,6 @@ export function signTransaction(args: { signer: Account; transaction: AnyRawTran return accountAuthenticator; } -export function signProofChallenge(args: { challenge: ProofChallenge; signer: Account }): Signature { - const { challenge, signer } = args; - const challengeHex = challenge.bcsToBytes(); - const signature = signer.sign(challengeHex); - return signature; -} - /** * Simulates a transaction before singing it. * @@ -357,7 +350,6 @@ export async function rotateAuthKey(args: { // Sign the challenge const challengeHex = challenge.bcsToBytes(); - const proofSignedByCurrentPrivateKey = fromAccount.sign(challengeHex); const proofSignedByNewPrivateKey = newAccount.sign(challengeHex); diff --git a/src/transactions/instances/proofChallenge.ts b/src/transactions/instances/proofChallenge.ts new file mode 100644 index 000000000..cfb0044af --- /dev/null +++ b/src/transactions/instances/proofChallenge.ts @@ -0,0 +1,16 @@ +import { Serializable, Serializer } from "../../bcs"; + +export class ProofChallenge extends Serializable { + public readonly data: Serializable[]; + + constructor(data: Serializable[]) { + super(); + this.data = data; + } + + serialize(serializer: Serializer): void { + this.data.forEach((data) => { + serializer.serialize(data); + }); + } +} diff --git a/src/transactions/instances/rotationProofChallenge.ts b/src/transactions/instances/rotationProofChallenge.ts index 8c899db1b..dcc2e6dc2 100644 --- a/src/transactions/instances/rotationProofChallenge.ts +++ b/src/transactions/instances/rotationProofChallenge.ts @@ -7,21 +7,6 @@ import { AnyNumber } from "../../types"; import { PublicKey } from "../../core/crypto"; import { MoveString, MoveVector, U64, U8 } from "../../bcs"; -export class ProofChallenge extends Serializable { - public readonly data: Serializable[]; - - constructor(data: Serializable[]) { - super(); - this.data = data; - } - - serialize(serializer: Serializer): void { - this.data.forEach((data) => { - serializer.serialize(data); - }); - } -} - /** * Representation of the challenge which is needed to sign by owner of the account * to rotate the authentication key. diff --git a/src/transactions/transactionBuilder/transactionBuilder.ts b/src/transactions/transactionBuilder/transactionBuilder.ts index 7193b9f07..0e963afc6 100644 --- a/src/transactions/transactionBuilder/transactionBuilder.ts +++ b/src/transactions/transactionBuilder/transactionBuilder.ts @@ -71,12 +71,22 @@ import { InputViewFunctionDataWithRemoteABI, InputViewFunctionDataWithABI, FunctionABI, + SimpleEntryFunctionArgumentTypes, } from "../types"; -import { convertArgument, fetchEntryFunctionAbi, fetchViewFunctionAbi, standardizeTypeTags } from "./remoteAbi"; +import { + convertArgument, + fetchEntryFunctionAbi, + fetchStructFieldsAbi, + fetchViewFunctionAbi, + standardizeTypeTags, +} from "./remoteAbi"; import { memoizeAsync } from "../../utils/memoize"; +import { AnyNumber, MoveFunctionId } from "../../types"; import { getFunctionParts, isScriptDataInput } from "./helpers"; import { SimpleTransaction } from "../instances/simpleTransaction"; import { MultiAgentTransaction } from "../instances/multiAgentTransaction"; +import { ProofChallenge } from "../instances/proofChallenge"; +import { MoveString } from "../../bcs"; /** * We are defining function signatures, each with its specific input and output. @@ -649,3 +659,34 @@ async function fetchAbi({ 1000 * 60 * 5, // 5 minutes )(); } + +export async function generateProofChallenge(args: { + config: AptosConfig; + struct: MoveFunctionId; + data: Array; +}) { + const { config, struct, data } = args; + const { moduleAddress, moduleName, functionName } = getFunctionParts(struct); + const structFieldsAbi = await fetchStructFieldsAbi(moduleAddress, moduleName, functionName, config); + + // Check all BCS types, and convert any non-BCS types + // TODO repeated code, move to a central place + const functionArguments: Array = + data.map((arg, i) => convertArgument(functionName, structFieldsAbi, arg, i, structFieldsAbi.parameters)) ?? []; + + // Check that all arguments are accounted for + if (functionArguments.length !== structFieldsAbi.parameters.length) { + throw new Error( + // eslint-disable-next-line max-len + `Too few arguments for '${moduleAddress}::${moduleName}::${functionName}', expected ${structFieldsAbi.parameters.length} but got ${functionArguments.length}`, + ); + } + + const challenge = new ProofChallenge([ + AccountAddress.from(moduleAddress), + new MoveString(moduleName), + new MoveString(functionName), + ...functionArguments, + ]); + return challenge; +} diff --git a/tests/e2e/api/proofChallenge.test.ts b/tests/e2e/api/proofChallenge.test.ts new file mode 100644 index 000000000..573f7ba75 --- /dev/null +++ b/tests/e2e/api/proofChallenge.test.ts @@ -0,0 +1,94 @@ +import { Account, U8, MoveVector, U64, AccountAddress } from "../../../src"; +import { getAptosClient } from "../helper"; + +const { aptos } = getAptosClient(); + +describe("proof challenge", () => { + test("generic challenge with simple arguments", async () => { + const fromAccount = Account.generate(); + const newAccount = Account.generate(); + + await aptos.fundAccount({ accountAddress: fromAccount.accountAddress, amount: 1_000_000_000 }); + await aptos.fundAccount({ accountAddress: newAccount.accountAddress, amount: 1_000_000_000 }); + + const accountInfo = await aptos.getAccountInfo({ + accountAddress: fromAccount.accountAddress, + }); + + const challenge = await aptos.createProofChallenge({ + struct: "0x1::account::RotationProofChallenge", + data: [ + BigInt(accountInfo.sequence_number), + fromAccount.accountAddress, + accountInfo.authentication_key, + newAccount.publicKey.toUint8Array(), + ], + }); + + const proofSignedByCurrentPrivateKey = aptos.signProofChallenge({ challenge, signer: fromAccount }); + const proofSignedByNewPrivateKey = aptos.signProofChallenge({ challenge, signer: newAccount }); + + const transaction = await aptos.transaction.build.simple({ + sender: fromAccount.accountAddress, + data: { + function: "0x1::account::rotate_authentication_key", + functionArguments: [ + new U8(fromAccount.signingScheme), // from scheme + MoveVector.U8(fromAccount.publicKey.toUint8Array()), + new U8(newAccount.signingScheme), // to scheme + MoveVector.U8(newAccount.publicKey.toUint8Array()), + MoveVector.U8(proofSignedByCurrentPrivateKey.toUint8Array()), + MoveVector.U8(proofSignedByNewPrivateKey.toUint8Array()), + ], + }, + }); + + const response = await aptos.signAndSubmitTransaction({ signer: fromAccount, transaction }); + const executedTransaction = await aptos.waitForTransaction({ transactionHash: response.hash }); + expect(executedTransaction.success).toBeTruthy(); + }); + + test("generic challenge with BCS arguments", async () => { + const fromAccount = Account.generate(); + const newAccount = Account.generate(); + + await aptos.fundAccount({ accountAddress: fromAccount.accountAddress, amount: 1_000_000_000 }); + await aptos.fundAccount({ accountAddress: newAccount.accountAddress, amount: 1_000_000_000 }); + + const accountInfo = await aptos.getAccountInfo({ + accountAddress: fromAccount.accountAddress, + }); + + const challenge = await aptos.createProofChallenge({ + struct: "0x1::account::RotationProofChallenge", + data: [ + new U64(BigInt(accountInfo.sequence_number)), + AccountAddress.from(fromAccount.accountAddress), + AccountAddress.from(accountInfo.authentication_key), + MoveVector.U8(newAccount.publicKey.toUint8Array()), + ], + }); + + const proofSignedByCurrentPrivateKey = aptos.signProofChallenge({ challenge, signer: fromAccount }); + const proofSignedByNewPrivateKey = aptos.signProofChallenge({ challenge, signer: newAccount }); + + const transaction = await aptos.transaction.build.simple({ + sender: fromAccount.accountAddress, + data: { + function: "0x1::account::rotate_authentication_key", + functionArguments: [ + new U8(fromAccount.signingScheme), // from scheme + MoveVector.U8(fromAccount.publicKey.toUint8Array()), + new U8(newAccount.signingScheme), // to scheme + MoveVector.U8(newAccount.publicKey.toUint8Array()), + MoveVector.U8(proofSignedByCurrentPrivateKey.toUint8Array()), + MoveVector.U8(proofSignedByNewPrivateKey.toUint8Array()), + ], + }, + }); + + const response = await aptos.signAndSubmitTransaction({ signer: fromAccount, transaction }); + const executedTransaction = await aptos.waitForTransaction({ transactionHash: response.hash }); + expect(executedTransaction.success).toBeTruthy(); + }); +}); diff --git a/tests/e2e/transaction/signTransaction.test.ts b/tests/e2e/transaction/signTransaction.test.ts index 236a64e64..a1b7dbf0b 100644 --- a/tests/e2e/transaction/signTransaction.test.ts +++ b/tests/e2e/transaction/signTransaction.test.ts @@ -6,8 +6,6 @@ import { AccountAuthenticator, AccountAuthenticatorEd25519, AccountAuthenticatorSingleKey, - MoveVector, - U8, } from "../../../src"; import { longTestTimeout } from "../../unit/helper"; import { getAptosClient } from "../helper"; @@ -218,46 +216,3 @@ describe("sign transaction", () => { }); }); }); - -test.only("test", async () => { - const fromAccount = Account.generate(); - const newAccount = Account.generate(); - - await aptos.fundAccount({ accountAddress: fromAccount.accountAddress, amount: 1_000_000_000 }); - await aptos.fundAccount({ accountAddress: newAccount.accountAddress, amount: 1_000_000_000 }); - - const accountInfo = await aptos.getAccountInfo({ - accountAddress: fromAccount.accountAddress, - }); - - const challenge = await aptos.createProofChallenge({ - struct: "0x1::account::RotationProofChallenge", - data: [ - BigInt(accountInfo.sequence_number), - fromAccount.accountAddress, - accountInfo.authentication_key, - newAccount.publicKey.toUint8Array(), - ], - }); - - const proofSignedByCurrentPrivateKey = aptos.signProofChallenge({ challenge, signer: fromAccount }); - const proofSignedByNewPrivateKey = aptos.signProofChallenge({ challenge, signer: newAccount }); - - const transaction = await aptos.transaction.build.simple({ - sender: fromAccount.accountAddress, - data: { - function: "0x1::account::rotate_authentication_key", - functionArguments: [ - new U8(fromAccount.signingScheme), // from scheme - MoveVector.U8(fromAccount.publicKey.toUint8Array()), - new U8(newAccount.signingScheme), // to scheme - MoveVector.U8(newAccount.publicKey.toUint8Array()), - MoveVector.U8(proofSignedByCurrentPrivateKey.toUint8Array()), - MoveVector.U8(proofSignedByNewPrivateKey.toUint8Array()), - ], - }, - }); - - const response = await aptos.signAndSubmitTransaction({ signer: fromAccount, transaction }); - console.log("response", response); -}); From 973ee3edcbe029b52aadc716922ea9e29ea0b00f Mon Sep 17 00:00:00 2001 From: maayan Date: Thu, 18 Apr 2024 10:25:21 +0300 Subject: [PATCH 3/3] deserilize arguments --- examples/typescript/proofChallenge.ts | 75 ++++++ src/api/proofChallenge.ts | 16 +- src/internal/proofChallenge.ts | 12 +- .../transactionBuilder/helpers.ts | 63 +++++ .../transactionBuilder/remoteAbi.ts | 123 +++++++++- .../transactionBuilder/transactionBuilder.ts | 41 +++- src/transactions/typeTag/index.ts | 31 +++ tests/e2e/api/proofChallenge.test.ts | 79 ++++-- tests/unit/remoteAbi.test.ts | 225 ++++++++++++++++++ 9 files changed, 631 insertions(+), 34 deletions(-) create mode 100644 examples/typescript/proofChallenge.ts diff --git a/examples/typescript/proofChallenge.ts b/examples/typescript/proofChallenge.ts new file mode 100644 index 000000000..67bc7ad88 --- /dev/null +++ b/examples/typescript/proofChallenge.ts @@ -0,0 +1,75 @@ +/* eslint-disable no-console */ +/* eslint-disable max-len */ + +import { Account, Aptos, AptosConfig, Network, NetworkToNetworkName, MoveVector, U8 } from "@aptos-labs/ts-sdk"; + +/** + * This example demonstrate the end-to-end flow of creating, signing and submitting + * a proog challenge to the Aptos chain + */ + +// Setup the client +const APTOS_NETWORK: Network = NetworkToNetworkName[process.env.APTOS_NETWORK ?? Network.LOCAL]; +const config = new AptosConfig({ network: APTOS_NETWORK }); +const aptos = new Aptos(config); + +async function main() { + // Create accounts + const fromAccount = Account.generate(); + const newAccount = Account.generate(); + + // Fund and create the accounts on chain + await aptos.fundAccount({ accountAddress: fromAccount.accountAddress, amount: 1_000_000_000 }); + await aptos.fundAccount({ accountAddress: newAccount.accountAddress, amount: 1_000_000_000 }); + + const accountInfo = await aptos.getAccountInfo({ + accountAddress: fromAccount.accountAddress, + }); + + // Create a rotation proof challenge. + const challenge = await aptos.createProofChallenge({ + struct: "0x1::account::RotationProofChallenge", + data: [ + BigInt(accountInfo.sequence_number), + fromAccount.accountAddress, + accountInfo.authentication_key, + newAccount.publicKey.toUint8Array(), + ], + }); + + // Display the challenge in a human readable format. This step is for + // any service who needs/wants to show the challenge to the account before + // they sign it. + const deserializedChallenge = await aptos.getProofChallenge({ + struct: "0x1::account::RotationProofChallenge", + data: challenge.bcsToBytes(), + }); + + console.log("rotation proof challenge to sign on", deserializedChallenge); + + // 1st account signs the challenge + const proofSignedByCurrentPrivateKey = aptos.signProofChallenge({ challenge, signer: fromAccount }); + // 2nd account signs the challenge + const proofSignedByNewPrivateKey = aptos.signProofChallenge({ challenge, signer: newAccount }); + + // Submit challenge to chain + const transaction = await aptos.transaction.build.simple({ + sender: fromAccount.accountAddress, + data: { + function: "0x1::account::rotate_authentication_key", + functionArguments: [ + new U8(fromAccount.signingScheme), // from scheme + MoveVector.U8(fromAccount.publicKey.toUint8Array()), + new U8(newAccount.signingScheme), // to scheme + MoveVector.U8(newAccount.publicKey.toUint8Array()), + MoveVector.U8(proofSignedByCurrentPrivateKey.toUint8Array()), + MoveVector.U8(proofSignedByNewPrivateKey.toUint8Array()), + ], + }, + }); + + const response = await aptos.signAndSubmitTransaction({ signer: fromAccount, transaction }); + await aptos.waitForTransaction({ transactionHash: response.hash }); +} + +main(); diff --git a/src/api/proofChallenge.ts b/src/api/proofChallenge.ts index b6d83bda2..2f82ddbf0 100644 --- a/src/api/proofChallenge.ts +++ b/src/api/proofChallenge.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { Account, Signature } from "../core"; -import { createProofChallenge, signProofChallenge } from "../internal/proofChallenge"; +import { createProofChallenge, getProofChallenge, signProofChallenge } from "../internal/proofChallenge"; import { MoveFunctionId } from "../types"; import { AptosConfig } from "./aptosConfig"; import { ProofChallenge as ProofChallengeInstance } from "../transactions/instances/proofChallenge"; @@ -36,6 +36,20 @@ export class ProofChallenge { }); } + /** + * Get the proog challenge in a human readable format + * + * @param args.struct The struct name + * @param args.data The serialized challenge + * @returns + */ + async getProofChallenge(args: { struct: MoveFunctionId; data: Uint8Array }) { + return getProofChallenge({ + config: this.config, + ...args, + }); + } + /** * Signs a generic proof challenge * diff --git a/src/internal/proofChallenge.ts b/src/internal/proofChallenge.ts index b21d3399c..767c2058f 100644 --- a/src/internal/proofChallenge.ts +++ b/src/internal/proofChallenge.ts @@ -1,6 +1,11 @@ import { AptosConfig } from "../api/aptosConfig"; import { Account, Signature } from "../core"; -import { EntryFunctionArgumentTypes, SimpleEntryFunctionArgumentTypes, generateProofChallenge } from "../transactions"; +import { + EntryFunctionArgumentTypes, + SimpleEntryFunctionArgumentTypes, + deserializeProofChallenge, + generateProofChallenge, +} from "../transactions"; import { ProofChallenge } from "../transactions/instances/proofChallenge"; import { MoveFunctionId } from "../types"; @@ -13,6 +18,11 @@ export async function createProofChallenge(args: { return challenge; } +export async function getProofChallenge(args: { config: AptosConfig; struct: MoveFunctionId; data: Uint8Array }) { + const challenge = deserializeProofChallenge({ ...args }); + return challenge; +} + export function signProofChallenge(args: { challenge: ProofChallenge; signer: Account }): Signature { const { challenge, signer } = args; const challengeHex = challenge.bcsToBytes(); diff --git a/src/transactions/transactionBuilder/helpers.ts b/src/transactions/transactionBuilder/helpers.ts index c79e2cdba..b868d77c2 100644 --- a/src/transactions/transactionBuilder/helpers.ts +++ b/src/transactions/transactionBuilder/helpers.ts @@ -11,6 +11,21 @@ import { import { Bool, FixedBytes, MoveOption, MoveString, MoveVector, U128, U16, U256, U32, U64, U8 } from "../../bcs"; import { AccountAddress } from "../../core"; import { MoveFunction, MoveFunctionId } from "../../types"; +import { + TypeTag, + TypeTagAddress, + TypeTagBool, + TypeTagGeneric, + TypeTagSigner, + TypeTagStruct, + TypeTagU128, + TypeTagU16, + TypeTagU256, + TypeTagU32, + TypeTagU64, + TypeTagU8, + TypeTagVector, +} from "../typeTag"; export function isBool(arg: SimpleEntryFunctionArgumentTypes): arg is boolean { return typeof arg === "boolean"; @@ -128,3 +143,51 @@ export function getFunctionParts(functionArg: MoveFunctionId) { const functionName = funcNameParts[2]; return { moduleAddress, moduleName, functionName }; } + +export function isTypeTagBool(param: TypeTag): boolean { + return param instanceof TypeTagBool; +} + +export function isTypeTagAddress(param: TypeTag): boolean { + return param instanceof TypeTagAddress; +} + +export function isTypeTagGeneric(param: TypeTag): boolean { + return param instanceof TypeTagGeneric; +} + +export function isTypeTagSigner(param: TypeTag): boolean { + return param instanceof TypeTagSigner; +} + +export function isTypeTagVector(param: TypeTag): boolean { + return param instanceof TypeTagVector; +} + +export function isTypeTagStruct(param: TypeTag): boolean { + return param instanceof TypeTagStruct; +} + +export function isTypeTagU8(param: TypeTag): boolean { + return param instanceof TypeTagU8; +} + +export function isTypeTagU16(param: TypeTag): boolean { + return param instanceof TypeTagU16; +} + +export function isTypeTagU32(param: TypeTag): boolean { + return param instanceof TypeTagU32; +} + +export function isTypeTagU64(param: TypeTag): boolean { + return param instanceof TypeTagU64; +} + +export function isTypeTagU128(param: TypeTag): boolean { + return param instanceof TypeTagU128; +} + +export function isTypeTagU256(param: TypeTag): boolean { + return param instanceof TypeTagU256; +} diff --git a/src/transactions/transactionBuilder/remoteAbi.ts b/src/transactions/transactionBuilder/remoteAbi.ts index 83bdc4e16..4a930d20a 100644 --- a/src/transactions/transactionBuilder/remoteAbi.ts +++ b/src/transactions/transactionBuilder/remoteAbi.ts @@ -11,8 +11,8 @@ import { ViewFunctionABI, FunctionABI, } from "../types"; -import { Bool, MoveOption, MoveString, MoveVector, U128, U16, U256, U32, U64, U8 } from "../../bcs"; -import { AccountAddress } from "../../core"; +import { Bool, Deserializer, MoveOption, MoveString, MoveVector, U128, U16, U256, U32, U64, U8 } from "../../bcs"; +import { AccountAddress, Ed25519PublicKey } from "../../core"; import { getModule } from "../../internal/account"; import { findFirstNonSignerArg, @@ -31,9 +31,18 @@ import { isNull, isNumber, isString, + isTypeTagAddress, + isTypeTagBool, + isTypeTagGeneric, + isTypeTagU128, + isTypeTagU16, + isTypeTagU256, + isTypeTagU32, + isTypeTagU64, + isTypeTagU8, throwTypeMismatch, } from "./helpers"; -import { MoveFunction } from "../../types"; +import { MoveFunction, MoveStruct } from "../../types"; const TEXT_ENCODER = new TextEncoder(); @@ -80,7 +89,7 @@ export async function fetchStructAbi( moduleName: string, structName: string, aptosConfig: AptosConfig, -) { +): Promise { // This fetch from the API is currently cached const module = await getModule({ aptosConfig, accountAddress: moduleAddress, moduleName }); @@ -181,7 +190,7 @@ export async function fetchStructFieldsAbi( moduleName: string, structName: string, aptosConfig: AptosConfig, -) { +): Promise { const structAbi = await fetchStructAbi(moduleAddress, moduleName, structName, aptosConfig); // If there's no ABI, then the function is invalid @@ -467,3 +476,107 @@ function checkType(param: TypeTag, arg: EntryFunctionArgumentTypes, position: nu throw new Error(`Type mismatch for argument ${position}, expected '${param.toString()}'`); } + +export function deserializeArgument( + params: Array, + deserializer: Deserializer, +): Array { + return params.map((param) => deserializeArg(deserializer, param)); +} + +export function deserializeArg(deserializer: Deserializer, param: TypeTag): SimpleEntryFunctionArgumentTypes { + if (isTypeTagBool(param)) { + return Bool.deserialize(deserializer).value; + } + if (isTypeTagAddress(param)) { + return AccountAddress.deserialize(deserializer).toString(); + } + if (isTypeTagU8(param)) { + return U8.deserialize(deserializer).value; + } + if (isTypeTagU16(param)) { + return U16.deserialize(deserializer).value; + } + if (isTypeTagU32(param)) { + return U32.deserialize(deserializer).value; + } + if (isTypeTagU64(param)) { + return U64.deserialize(deserializer).value; + } + if (isTypeTagU128(param)) { + return U128.deserialize(deserializer).value; + } + if (isTypeTagU256(param)) { + return U256.deserialize(deserializer).value; + } + if (isTypeTagGeneric(param)) { + // // Currently, TS SDK `deserialize` can only handle a single class, not a class with generics + throw new Error("Generic type deserialization is not implemented"); + } + + if (param.isVector()) { + if (isTypeTagU8(param.value)) { + // TODO handle Secp256k1PublicKey + const { values } = MoveVector.deserialize(deserializer, U8); + const numbers = values.map((value) => value.value); + try { + return new Ed25519PublicKey(new Uint8Array(numbers)).toString(); + } catch (e: any) { + return numbers; + } + } + if (isTypeTagU16(param.value)) { + const { values } = MoveVector.deserialize(deserializer, U16); + return values.map((value) => value.value); + } + if (isTypeTagU32(param.value)) { + const { values } = MoveVector.deserialize(deserializer, U32); + return values.map((value) => value.value); + } + if (isTypeTagU64(param.value)) { + const { values } = MoveVector.deserialize(deserializer, U64); + return values.map((value) => value.value); + } + if (isTypeTagU128(param.value)) { + const { values } = MoveVector.deserialize(deserializer, U128); + return values.map((value) => value.value); + } + if (isTypeTagU256(param.value)) { + const { values } = MoveVector.deserialize(deserializer, U256); + return values.map((value) => value.value); + } + if (isTypeTagBool(param.value)) { + const { values } = MoveVector.deserialize(deserializer, Bool); + return values.map((value) => value.value); + } + if (isTypeTagAddress(param.value)) { + const { values } = MoveVector.deserialize(deserializer, AccountAddress); + return values.map((value) => value.toString()); + } + if (param.value.isStruct()) { + if (param.value.isObject()) { + const { values } = MoveVector.deserialize(deserializer, AccountAddress); + return values.map((value) => value.toString()); + } + if (param.value.isOption()) { + // Currently, TS SDK `deserialize` can only handle a single class, not a class with generics + throw new Error("Option type deserialization is not implemented"); + } + + const { values } = MoveVector.deserialize(deserializer, MoveString); + return values.map((value) => value.value); + } + } + if (param.isStruct()) { + if (param.isObject()) { + return AccountAddress.deserialize(deserializer).toString(); + } + if (param.isOption()) { + // Currently, TS SDK `deserialize` can only handle a single class, not a class with generics + throw new Error("Option type deserialization is not implemented"); + } + return MoveString.deserialize(deserializer).value; + } + + throw new Error(`Could not deserialize type '${param.toString()}'`); +} diff --git a/src/transactions/transactionBuilder/transactionBuilder.ts b/src/transactions/transactionBuilder/transactionBuilder.ts index 0e963afc6..aad3bde12 100644 --- a/src/transactions/transactionBuilder/transactionBuilder.ts +++ b/src/transactions/transactionBuilder/transactionBuilder.ts @@ -75,18 +75,19 @@ import { } from "../types"; import { convertArgument, + deserializeArgument, fetchEntryFunctionAbi, fetchStructFieldsAbi, fetchViewFunctionAbi, standardizeTypeTags, } from "./remoteAbi"; import { memoizeAsync } from "../../utils/memoize"; -import { AnyNumber, MoveFunctionId } from "../../types"; +import { MoveFunctionId } from "../../types"; import { getFunctionParts, isScriptDataInput } from "./helpers"; import { SimpleTransaction } from "../instances/simpleTransaction"; import { MultiAgentTransaction } from "../instances/multiAgentTransaction"; import { ProofChallenge } from "../instances/proofChallenge"; -import { MoveString } from "../../bcs"; +import { Deserializer, MoveString } from "../../bcs"; /** * We are defining function signatures, each with its specific input and output. @@ -671,14 +672,14 @@ export async function generateProofChallenge(args: { // Check all BCS types, and convert any non-BCS types // TODO repeated code, move to a central place - const functionArguments: Array = + const structArguments: Array = data.map((arg, i) => convertArgument(functionName, structFieldsAbi, arg, i, structFieldsAbi.parameters)) ?? []; // Check that all arguments are accounted for - if (functionArguments.length !== structFieldsAbi.parameters.length) { + if (structArguments.length !== structFieldsAbi.parameters.length) { throw new Error( // eslint-disable-next-line max-len - `Too few arguments for '${moduleAddress}::${moduleName}::${functionName}', expected ${structFieldsAbi.parameters.length} but got ${functionArguments.length}`, + `Too few arguments for '${moduleAddress}::${moduleName}::${functionName}', expected ${structFieldsAbi.parameters.length} but got ${structArguments.length}`, ); } @@ -686,7 +687,35 @@ export async function generateProofChallenge(args: { AccountAddress.from(moduleAddress), new MoveString(moduleName), new MoveString(functionName), - ...functionArguments, + ...structArguments, ]); return challenge; } + +export async function deserializeProofChallenge(args: { + config: AptosConfig; + struct: MoveFunctionId; + data: Uint8Array; +}) { + const { config, struct, data } = args; + const { moduleAddress, moduleName, functionName } = getFunctionParts(struct); + + const structFieldsAbi = await fetchStructFieldsAbi(moduleAddress, moduleName, functionName, config); + + const deserializer = new Deserializer(data); + + // First 3 values are always the struct address in the format + // of `${moduleAddress}::${moduleName}::${structName}` + const deserializedModuleAddress = AccountAddress.deserialize(deserializer); + const deserializedModuleName = MoveString.deserialize(deserializer); + const deserializedfunctionName = MoveString.deserialize(deserializer); + const structName = `${deserializedModuleAddress.toString()}::${deserializedModuleName.value}::${ + deserializedfunctionName.value + }`; + + const functionArguments: Array = deserializeArgument( + structFieldsAbi.parameters, + deserializer, + ); + return { structName, functionArguments }; +} diff --git a/src/transactions/typeTag/index.ts b/src/transactions/typeTag/index.ts index 59f6c9b09..d16d36bcd 100644 --- a/src/transactions/typeTag/index.ts +++ b/src/transactions/typeTag/index.ts @@ -9,6 +9,7 @@ import { Serializable, Serializer } from "../../bcs/serializer"; import { AccountAddress } from "../../core"; import { Identifier } from "../instances/identifier"; import { TypeTagVariants } from "../../types"; +import { U128, U16, U256, U32, U64, U8 } from "../../bcs"; export abstract class TypeTag extends Serializable { abstract serialize(serializer: Serializer): void; @@ -388,3 +389,33 @@ export function optionStructTag(typeArg: TypeTag): StructTag { export function objectStructTag(typeArg: TypeTag): StructTag { return new StructTag(AccountAddress.ONE, new Identifier("object"), new Identifier("Object"), [typeArg]); } + +export const convert = ( + value: string, +): typeof U8 | typeof U16 | typeof U32 | typeof U64 | typeof U128 | typeof U256 => { + if (value === "u8") { + return U8; + } + + if (value === "u16") { + return U16; + } + + if (value === "u32") { + return U32; + } + + if (value === "u64") { + return U64; + } + + if (value === "u128") { + return U128; + } + + if (value === "u256") { + return U256; + } + + throw new Error(""); +}; diff --git a/tests/e2e/api/proofChallenge.test.ts b/tests/e2e/api/proofChallenge.test.ts index 573f7ba75..35b2960df 100644 --- a/tests/e2e/api/proofChallenge.test.ts +++ b/tests/e2e/api/proofChallenge.test.ts @@ -1,15 +1,15 @@ import { Account, U8, MoveVector, U64, AccountAddress } from "../../../src"; +import { ProofChallenge } from "../../../src/transactions/instances/proofChallenge"; import { getAptosClient } from "../helper"; const { aptos } = getAptosClient(); describe("proof challenge", () => { - test("generic challenge with simple arguments", async () => { + test("it creates generic challenge with simple arguments", async () => { const fromAccount = Account.generate(); const newAccount = Account.generate(); await aptos.fundAccount({ accountAddress: fromAccount.accountAddress, amount: 1_000_000_000 }); - await aptos.fundAccount({ accountAddress: newAccount.accountAddress, amount: 1_000_000_000 }); const accountInfo = await aptos.getAccountInfo({ accountAddress: fromAccount.accountAddress, @@ -25,30 +25,33 @@ describe("proof challenge", () => { ], }); - const proofSignedByCurrentPrivateKey = aptos.signProofChallenge({ challenge, signer: fromAccount }); - const proofSignedByNewPrivateKey = aptos.signProofChallenge({ challenge, signer: newAccount }); + expect(challenge instanceof ProofChallenge).toBeTruthy(); + }); - const transaction = await aptos.transaction.build.simple({ - sender: fromAccount.accountAddress, - data: { - function: "0x1::account::rotate_authentication_key", - functionArguments: [ - new U8(fromAccount.signingScheme), // from scheme - MoveVector.U8(fromAccount.publicKey.toUint8Array()), - new U8(newAccount.signingScheme), // to scheme - MoveVector.U8(newAccount.publicKey.toUint8Array()), - MoveVector.U8(proofSignedByCurrentPrivateKey.toUint8Array()), - MoveVector.U8(proofSignedByNewPrivateKey.toUint8Array()), - ], - }, + test("it creates generic challenge with BCS arguments", async () => { + const fromAccount = Account.generate(); + const newAccount = Account.generate(); + + await aptos.fundAccount({ accountAddress: fromAccount.accountAddress, amount: 1_000_000_000 }); + + const accountInfo = await aptos.getAccountInfo({ + accountAddress: fromAccount.accountAddress, }); - const response = await aptos.signAndSubmitTransaction({ signer: fromAccount, transaction }); - const executedTransaction = await aptos.waitForTransaction({ transactionHash: response.hash }); - expect(executedTransaction.success).toBeTruthy(); + const challenge = await aptos.createProofChallenge({ + struct: "0x1::account::RotationProofChallenge", + data: [ + new U64(BigInt(accountInfo.sequence_number)), + AccountAddress.from(fromAccount.accountAddress), + AccountAddress.from(accountInfo.authentication_key), + MoveVector.U8(newAccount.publicKey.toUint8Array()), + ], + }); + + expect(challenge instanceof ProofChallenge).toBeTruthy(); }); - test("generic challenge with BCS arguments", async () => { + test("gets generic challenge", async () => { const fromAccount = Account.generate(); const newAccount = Account.generate(); @@ -69,6 +72,40 @@ describe("proof challenge", () => { ], }); + const deserializedChallenge = await aptos.getProofChallenge({ + struct: "0x1::account::RotationProofChallenge", + data: challenge.bcsToBytes(), + }); + + expect(deserializedChallenge.structName).toEqual("0x1::account::RotationProofChallenge"); + expect(deserializedChallenge.functionArguments[0]).toEqual(0n); + expect(deserializedChallenge.functionArguments[1]).toEqual(fromAccount.accountAddress.toString()); + // account authentication_key is the account address + expect(deserializedChallenge.functionArguments[2]).toEqual(fromAccount.accountAddress.toString()); + expect(deserializedChallenge.functionArguments[3]).toEqual(newAccount.publicKey.toString()); + }); + + test("it submits generic challenge transaction", async () => { + const fromAccount = Account.generate(); + const newAccount = Account.generate(); + + await aptos.fundAccount({ accountAddress: fromAccount.accountAddress, amount: 1_000_000_000 }); + await aptos.fundAccount({ accountAddress: newAccount.accountAddress, amount: 1_000_000_000 }); + + const accountInfo = await aptos.getAccountInfo({ + accountAddress: fromAccount.accountAddress, + }); + + const challenge = await aptos.createProofChallenge({ + struct: "0x1::account::RotationProofChallenge", + data: [ + BigInt(accountInfo.sequence_number), + fromAccount.accountAddress, + accountInfo.authentication_key, + newAccount.publicKey.toUint8Array(), + ], + }); + const proofSignedByCurrentPrivateKey = aptos.signProofChallenge({ challenge, signer: fromAccount }); const proofSignedByNewPrivateKey = aptos.signProofChallenge({ challenge, signer: newAccount }); diff --git a/tests/unit/remoteAbi.test.ts b/tests/unit/remoteAbi.test.ts index c0af03916..6f052ad39 100644 --- a/tests/unit/remoteAbi.test.ts +++ b/tests/unit/remoteAbi.test.ts @@ -1,12 +1,24 @@ import { + Account, AccountAddress, Bool, checkOrConvertArgument, + deserializeArgument, + Deserializer, + Identifier, MoveOption, MoveString, MoveVector, parseTypeTag, + StructTag, + TypeTagAddress, + TypeTagBool, + TypeTagStruct, + TypeTagU128, + TypeTagU16, TypeTagU256, + TypeTagU32, + TypeTagU64, TypeTagU8, TypeTagVector, U128, @@ -276,4 +288,217 @@ describe("Remote ABI", () => { // TODO: Verify string behavior on u64 and above }); }); + describe("deserialize", () => { + describe("primitives", () => { + test("should deserialize TypeTagBool", () => { + const bool = new Bool(true).bcsToBytes(); + const typeTagBool = new TypeTagBool(); + + const deserializer = new Deserializer(bool); + const data = deserializeArgument([typeTagBool], deserializer); + expect(data[0]).toEqual(true); + }); + test("should deserialize TypeTagU8", () => { + const u8 = new U8(MAX_U8).bcsToBytes(); + const typeTagBool = new TypeTagU8(); + + const deserializer = new Deserializer(u8); + const data = deserializeArgument([typeTagBool], deserializer); + expect(data[0]).toEqual(MAX_U8); + }); + test("should deserialize TypeTagU16", () => { + const u16 = new U16(MAX_U16).bcsToBytes(); + const typeTagBool = new TypeTagU16(); + + const deserializer = new Deserializer(u16); + const data = deserializeArgument([typeTagBool], deserializer); + expect(data[0]).toEqual(MAX_U16); + }); + test("should deserialize TypeTagU32", () => { + const u32 = new U32(MAX_U32).bcsToBytes(); + const typeTagBool = new TypeTagU32(); + + const deserializer = new Deserializer(u32); + const data = deserializeArgument([typeTagBool], deserializer); + expect(data[0]).toEqual(MAX_U32); + }); + test("should deserialize TypeTagU64", () => { + const u64 = new U64(MAX_U64).bcsToBytes(); + const typeTagBool = new TypeTagU64(); + + const deserializer = new Deserializer(u64); + const data = deserializeArgument([typeTagBool], deserializer); + expect(data[0]).toEqual(MAX_U64); + }); + test("should deserialize TypeTagU128", () => { + const u128 = new U128(MAX_U128).bcsToBytes(); + const typeTagBool = new TypeTagU128(); + + const deserializer = new Deserializer(u128); + const data = deserializeArgument([typeTagBool], deserializer); + expect(data[0]).toEqual(MAX_U128); + }); + test("should deserialize TypeTagU256", () => { + const u256 = new U256(MAX_U256).bcsToBytes(); + const typeTagBool = new TypeTagU256(); + + const deserializer = new Deserializer(u256); + const data = deserializeArgument([typeTagBool], deserializer); + expect(data[0]).toEqual(MAX_U256); + }); + }); + + describe("address", () => { + test("should deserialize TypeTagAddress", () => { + const address = AccountAddress.ONE.bcsToBytes(); + const typeTagAddress = new TypeTagAddress(); + + const deserializer = new Deserializer(address); + const data = deserializeArgument([typeTagAddress], deserializer); + expect(data[0]).toEqual(AccountAddress.ONE.toString()); + }); + }); + + describe("struct", () => { + test("should deserialize TypeTagStruct string", () => { + const string = new MoveString("Hello Aptos").bcsToBytes(); + const structTag = new StructTag(AccountAddress.ONE, new Identifier("string"), new Identifier("String"), []); + const typeTagStruct = new TypeTagStruct(structTag); + + const deserializer = new Deserializer(string); + const data = deserializeArgument([typeTagStruct], deserializer); + expect(data[0]).toEqual("Hello Aptos"); + }); + test("should deserialize TypeTagStruct object", () => { + const object = AccountAddress.ONE.bcsToBytes(); + const structTag = new StructTag(AccountAddress.ONE, new Identifier("object"), new Identifier("Object"), []); + const typeTagStruct = new TypeTagStruct(structTag); + + const deserializer = new Deserializer(object); + const data = deserializeArgument([typeTagStruct], deserializer); + expect(data[0]).toEqual(AccountAddress.ONE.toString()); + }); + + test("should deserialize TypeTagStruct struct", () => { + const struct = new MoveString("0x123::aptos:SDK").bcsToBytes(); + + const structTag = new StructTag( + AccountAddress.from("0x123"), + new Identifier("aptos"), + new Identifier("SDK"), + [], + ); + const typeTagStruct = new TypeTagStruct(structTag); + + const deserializer = new Deserializer(struct); + const data = deserializeArgument([typeTagStruct], deserializer); + expect(data[0]).toEqual("0x123::aptos:SDK"); + }); + }); + + describe.only("vector", () => { + test("should deserialize vector of U8", () => { + const u8 = new MoveVector([new U8(MAX_U8)]).bcsToBytes(); + const typeTagVector = new TypeTagVector(new TypeTagU8()); + + const deserializer = new Deserializer(u8); + const data = deserializeArgument([typeTagVector], deserializer); + expect(data[0]).toEqual([MAX_U8]); + }); + + test("should deserialize ed25519 public key as a vector of U8", () => { + const account = Account.generate(); + const publicKeyArray = MoveVector.U8(account.publicKey.toUint8Array()).bcsToBytes(); + const typeTagVector = new TypeTagVector(new TypeTagU8()); + + const deserializer = new Deserializer(publicKeyArray); + const data = deserializeArgument([typeTagVector], deserializer); + expect(data[0]).toEqual(account.publicKey.toString()); + }); + + test("should deserialize vector of U16", () => { + const u16 = new MoveVector([new U16(MAX_U16)]).bcsToBytes(); + const typeTagVector = new TypeTagVector(new TypeTagU16()); + + const deserializer = new Deserializer(u16); + const data = deserializeArgument([typeTagVector], deserializer); + expect(data[0]).toEqual([MAX_U16]); + }); + + test("should deserialize vector of U32", () => { + const u32 = new MoveVector([new U32(MAX_U32)]).bcsToBytes(); + const typeTagVector = new TypeTagVector(new TypeTagU32()); + + const deserializer = new Deserializer(u32); + const data = deserializeArgument([typeTagVector], deserializer); + expect(data[0]).toEqual([MAX_U32]); + }); + + test("should deserialize vector of U64", () => { + const u64 = new MoveVector([new U64(MAX_U64)]).bcsToBytes(); + const typeTagVector = new TypeTagVector(new TypeTagU64()); + + const deserializer = new Deserializer(u64); + const data = deserializeArgument([typeTagVector], deserializer); + expect(data[0]).toEqual([MAX_U64]); + }); + + test("should deserialize vector of U128", () => { + const u128 = new MoveVector([new U128(MAX_U128)]).bcsToBytes(); + const typeTagVector = new TypeTagVector(new TypeTagU128()); + + const deserializer = new Deserializer(u128); + const data = deserializeArgument([typeTagVector], deserializer); + expect(data[0]).toEqual([MAX_U128]); + }); + + test("should deserialize vector of U256", () => { + const u256 = new MoveVector([new U256(MAX_U256)]).bcsToBytes(); + const typeTagVector = new TypeTagVector(new TypeTagU256()); + + const deserializer = new Deserializer(u256); + const data = deserializeArgument([typeTagVector], deserializer); + expect(data[0]).toEqual([MAX_U256]); + }); + + test("should deserialize vector of bool", () => { + const bool = new MoveVector([new Bool(true)]).bcsToBytes(); + const typeTagBool = new TypeTagVector(new TypeTagBool()); + + const deserializer = new Deserializer(bool); + const data = deserializeArgument([typeTagBool], deserializer); + expect(data[0]).toEqual([true]); + }); + + test("should deserialize vector of address", () => { + const account = Account.generate(); + const address = new MoveVector([new AccountAddress(account.accountAddress.toUint8Array())]).bcsToBytes(); + const typeTagAddress = new TypeTagVector(new TypeTagAddress()); + + const deserializer = new Deserializer(address); + const data = deserializeArgument([typeTagAddress], deserializer); + expect(data[0]).toEqual([account.accountAddress.toString()]); + }); + + test("should deserialize vector of strings", () => { + const stringArray = new MoveVector([new MoveString("Hello Aptos")]).bcsToBytes(); + const structTag = new StructTag(AccountAddress.ONE, new Identifier("string"), new Identifier("String"), []); + const typeTagVector = new TypeTagVector(new TypeTagStruct(structTag)); + + const deserializer = new Deserializer(stringArray); + const data = deserializeArgument([typeTagVector], deserializer); + expect(data[0]).toEqual(["Hello Aptos"]); + }); + + test("should deserialize vector of objects", () => { + const stringArray = new MoveVector([AccountAddress.ONE]).bcsToBytes(); + const structTag = new StructTag(AccountAddress.ONE, new Identifier("object"), new Identifier("Object"), []); + const typeTagVector = new TypeTagVector(new TypeTagStruct(structTag)); + + const deserializer = new Deserializer(stringArray); + const data = deserializeArgument([typeTagVector], deserializer); + expect(data[0]).toEqual([AccountAddress.ONE.toString()]); + }); + }); + }); });