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/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/proofChallenge.ts b/src/api/proofChallenge.ts new file mode 100644 index 000000000..2f82ddbf0 --- /dev/null +++ b/src/api/proofChallenge.ts @@ -0,0 +1,67 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import { Account, Signature } from "../core"; +import { createProofChallenge, getProofChallenge, 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, + }); + } + + /** + * 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 + * + * @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/internal/proofChallenge.ts b/src/internal/proofChallenge.ts new file mode 100644 index 000000000..767c2058f --- /dev/null +++ b/src/internal/proofChallenge.ts @@ -0,0 +1,31 @@ +import { AptosConfig } from "../api/aptosConfig"; +import { Account, Signature } from "../core"; +import { + EntryFunctionArgumentTypes, + SimpleEntryFunctionArgumentTypes, + deserializeProofChallenge, + 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 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(); + const signature = signer.sign(challengeHex); + return signature; +} 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/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 cee854e29..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(); @@ -75,6 +84,22 @@ export async function fetchFunctionAbi( return undefined; } +export async function fetchStructAbi( + moduleAddress: string, + moduleName: string, + structName: string, + aptosConfig: AptosConfig, +): Promise { + // 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 +185,30 @@ export async function fetchViewFunctionAbi( }; } +export async function fetchStructFieldsAbi( + moduleAddress: string, + 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 + 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 @@ -427,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 7193b9f07..aad3bde12 100644 --- a/src/transactions/transactionBuilder/transactionBuilder.ts +++ b/src/transactions/transactionBuilder/transactionBuilder.ts @@ -71,12 +71,23 @@ import { InputViewFunctionDataWithRemoteABI, InputViewFunctionDataWithABI, FunctionABI, + SimpleEntryFunctionArgumentTypes, } from "../types"; -import { convertArgument, fetchEntryFunctionAbi, fetchViewFunctionAbi, standardizeTypeTags } from "./remoteAbi"; +import { + convertArgument, + deserializeArgument, + fetchEntryFunctionAbi, + fetchStructFieldsAbi, + fetchViewFunctionAbi, + standardizeTypeTags, +} from "./remoteAbi"; import { memoizeAsync } from "../../utils/memoize"; +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 { Deserializer, MoveString } from "../../bcs"; /** * We are defining function signatures, each with its specific input and output. @@ -649,3 +660,62 @@ 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 structArguments: Array = + data.map((arg, i) => convertArgument(functionName, structFieldsAbi, arg, i, structFieldsAbi.parameters)) ?? []; + + // Check that all arguments are accounted for + 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 ${structArguments.length}`, + ); + } + + const challenge = new ProofChallenge([ + AccountAddress.from(moduleAddress), + new MoveString(moduleName), + new MoveString(functionName), + ...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 new file mode 100644 index 000000000..35b2960df --- /dev/null +++ b/tests/e2e/api/proofChallenge.test.ts @@ -0,0 +1,131 @@ +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("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 }); + + 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(), + ], + }); + + expect(challenge instanceof ProofChallenge).toBeTruthy(); + }); + + 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 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("gets generic challenge", 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 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 }); + + 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/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()]); + }); + }); + }); });