From 586031671179268d109fa128da18f08e3560dbf9 Mon Sep 17 00:00:00 2001 From: danielailie Date: Mon, 18 Nov 2024 14:25:26 +0200 Subject: [PATCH 1/3] Add entrypoints --- src/entrypoints/config.ts | 29 ++++++ src/entrypoints/entrypoints.spec.ts | 119 +++++++++++++++++++++++ src/entrypoints/entrypoints.ts | 142 ++++++++++++++++++++++++++++ src/entrypoints/index.ts | 2 + src/errors.ts | 9 ++ src/index.ts | 1 + src/message.ts | 7 +- 7 files changed, 305 insertions(+), 4 deletions(-) create mode 100644 src/entrypoints/config.ts create mode 100644 src/entrypoints/entrypoints.spec.ts create mode 100644 src/entrypoints/entrypoints.ts create mode 100644 src/entrypoints/index.ts diff --git a/src/entrypoints/config.ts b/src/entrypoints/config.ts new file mode 100644 index 00000000..039fbbc3 --- /dev/null +++ b/src/entrypoints/config.ts @@ -0,0 +1,29 @@ +export interface EntrypointConfig { + networkProviderUrl: string; + networkProviderKind: string; + chainId: string; +} + +export class TestnetEntrypointConfig implements EntrypointConfig { + networkProviderUrl = "https://testnet-api.multiversx.com"; + networkProviderKind = "api"; + chainId = "T"; +} + +export class DevnetEntrypointConfig implements EntrypointConfig { + networkProviderUrl = "https://devnet-api.multiversx.com"; + networkProviderKind = "api"; + chainId = "D"; +} + +export class MainnetEntrypointConfig implements EntrypointConfig { + networkProviderUrl = "https://api.multiversx.com"; + networkProviderKind = "api"; + chainId = "1"; +} + +export class LocalnetEntrypointConfig implements EntrypointConfig { + networkProviderUrl = "http://localhost:7950"; + networkProviderKind = "proxy"; + chainId = "localnet"; +} diff --git a/src/entrypoints/entrypoints.spec.ts b/src/entrypoints/entrypoints.spec.ts new file mode 100644 index 00000000..43937c65 --- /dev/null +++ b/src/entrypoints/entrypoints.spec.ts @@ -0,0 +1,119 @@ +import { assert } from "chai"; +import { readFileSync } from "fs"; +import { Account } from "../accounts/account"; +import { Address } from "../address"; +import { loadAbiRegistry, loadTestWallet, TestWallet } from "../testutils"; +import { TransactionComputer } from "../transactionComputer"; +import { DevnetEntrypoint } from "./entrypoints"; + +describe("TestEntrypoint", () => { + const entrypoint = new DevnetEntrypoint(); + let alicePem: TestWallet; + let bobPem: TestWallet; + let txComputer: TransactionComputer; + + before(async function () { + alicePem = await loadTestWallet("alice"); + bobPem = await loadTestWallet("bob"); + txComputer = new TransactionComputer(); + }); + + it("native transfer", async () => { + const controller = entrypoint.createTransfersController(); + const sender = Account.newFromPem(alicePem.pemFileText); + sender.nonce = 77777; + + const transaction = await controller.createTransactionForTransfer( + sender, + BigInt(sender.getNonceThenIncrement().valueOf()), + { + receiver: sender.address, + nativeAmount: BigInt(0), + data: Buffer.from("hello"), + }, + ); + assert.equal( + Buffer.from(transaction.signature).toString("hex"), + "69bc7d1777edd0a901e6cf94830475716205c5efdf2fd44d4be31badead59fc8418b34f0aa3b2c80ba14aed5edd30031757d826af58a1abb690a0bee89ba9309", + ); + }); + + it("contract flow", async function () { + this.timeout(30000); + const abi = await loadAbiRegistry("src/testdata/adder.abi.json"); + const sender = Account.newFromPem(alicePem.pemFileText); + const accountAddress = new Address(sender.address.bech32()); + sender.nonce = await entrypoint.recallAccountNonce(accountAddress); + + const controller = entrypoint.createSmartContractController(abi); + const bytecode = readFileSync("src/testdata/adder.wasm"); + + const transaction = await controller.createTransactionForDeploy( + sender, + BigInt(sender.getNonceThenIncrement().valueOf()), + { + bytecode, + gasLimit: BigInt(10_000_000), + arguments: [0], + }, + ); + + const txHash = await entrypoint.sendTransaction(transaction); + const outcome = await controller.awaitCompletedDeploy(txHash); + + assert.equal(outcome.contracts.length, 1); + + const contractAddress = Address.fromBech32(outcome.contracts[0].address); + + const executeTransaction = await controller.createTransactionForExecute( + sender, + BigInt(sender.getNonceThenIncrement().valueOf()), + { + contract: contractAddress, + gasLimit: BigInt(10_000_000), + function: "add", + arguments: [7], + }, + ); + + const txHashExecute = await entrypoint.sendTransaction(executeTransaction); + await entrypoint.awaitCompletedTransaction(txHashExecute); + + const queryResult = await controller.queryContract(contractAddress, "getSum", []); + assert.equal(queryResult.length, 1); + assert.equal(queryResult[0], 7); + }); + + it("create relayed transaction", async function () { + const transferController = entrypoint.createTransfersController(); + const sender = Account.newFromPem(alicePem.pemFileText); + sender.nonce = 77777; + + const relayer = Account.newFromPem(bobPem.pemFileText); + relayer.nonce = 7; + + const transaction = await transferController.createTransactionForTransfer( + sender, + BigInt(sender.getNonceThenIncrement().valueOf()), + { + receiver: sender.address, + data: Buffer.from("hello"), + }, + ); + const innerTransactionGasLimit = transaction.gasLimit; + transaction.gasLimit = BigInt(0); + transaction.signature = await sender.sign(txComputer.computeBytesForSigning(transaction)); + + const relayedController = entrypoint.createRelayedController(); + const relayedTransaction = relayedController.createRelayedV2Transaction( + relayer, + BigInt(relayer.getNonceThenIncrement().valueOf()), + { + innerTransaction: transaction, + innerTransactionGasLimit, + }, + ); + + assert.equal((await relayedTransaction).chainID, "D"); + }); +}); diff --git a/src/entrypoints/entrypoints.ts b/src/entrypoints/entrypoints.ts new file mode 100644 index 00000000..06379dfb --- /dev/null +++ b/src/entrypoints/entrypoints.ts @@ -0,0 +1,142 @@ +import { AbiRegistry } from "../abi"; +import { AccountController } from "../accountManagement"; +import { IAccount } from "../accounts/interfaces"; +import { Address } from "../address"; +import { DelegationController } from "../delegation"; +import { ErrInvalidNetworkProviderKind } from "../errors"; +import { Message, MessageComputer } from "../message"; +import { ApiNetworkProvider, ProxyNetworkProvider, TransactionOnNetwork } from "../networkProviders"; +import { RelayedController } from "../relayed/relayedController"; +import { SmartContractController } from "../smartContracts/smartContractController"; +import { TokenManagementController } from "../tokenManagement"; +import { Transaction } from "../transaction"; +import { TransactionComputer } from "../transactionComputer"; +import { TransactionWatcher } from "../transactionWatcher"; +import { TransfersController } from "../transfers/transfersControllers"; +import { UserVerifier } from "../wallet"; +import { DevnetEntrypointConfig, MainnetEntrypointConfig, TestnetEntrypointConfig } from "./config"; + +class NetworkEntrypoint { + private networkProvider: ApiNetworkProvider | ProxyNetworkProvider; + private chainId: string; + + constructor(networkProviderUrl: string, networkProviderKind: string, chainId: string) { + if (networkProviderKind === "proxy") { + this.networkProvider = new ProxyNetworkProvider(networkProviderUrl); + } else if (networkProviderKind === "api") { + this.networkProvider = new ApiNetworkProvider(networkProviderUrl); + } else { + throw new ErrInvalidNetworkProviderKind(); + } + + this.chainId = chainId; + } + + async signTransaction(transaction: Transaction, account: IAccount): Promise { + const txComputer = new TransactionComputer(); + transaction.signature = await account.sign(txComputer.computeBytesForSigning(transaction)); + } + + verifyTransactionSignature(transaction: Transaction): boolean { + const verifier = UserVerifier.fromAddress(Address.fromBech32(transaction.sender)); + const txComputer = new TransactionComputer(); + return verifier.verify(txComputer.computeBytesForVerifying(transaction), transaction.signature); + } + + async signMessage(message: Message, account: IAccount): Promise { + const messageComputer = new MessageComputer(); + message.signature = await account.sign(messageComputer.computeBytesForSigning(message)); + } + + verifyMessageSignature(message: Message): boolean { + if (!message.address) { + throw new Error("`address` property of Message is not set"); + } + + if (!message.signature) { + throw new Error("`signature` property of Message is not set"); + } + + const verifier = UserVerifier.fromAddress(message.address); + const messageComputer = new MessageComputer(); + return verifier.verify(messageComputer.computeBytesForVerifying(message), message.signature); + } + + async recallAccountNonce(address: Address): Promise { + return (await this.networkProvider.getAccount(address)).nonce; + } + + sendTransactions(transactions: Transaction[]): Promise { + return this.networkProvider.sendTransactions(transactions); + } + + sendTransaction(transaction: Transaction): Promise { + return this.networkProvider.sendTransaction(transaction); + } + + async awaitCompletedTransaction(txHash: string): Promise { + const transactionAwaiter = new TransactionWatcher(this.networkProvider); + return transactionAwaiter.awaitCompleted(txHash); + } + + createNetworkProvider(): ApiNetworkProvider | ProxyNetworkProvider { + return this.networkProvider; + } + + createDelegationController(): DelegationController { + return new DelegationController({ chainID: this.chainId, networkProvider: this.networkProvider }); + } + + createAccountController(): AccountController { + return new AccountController({ chainID: this.chainId }); + } + + createRelayedController(): RelayedController { + return new RelayedController({ chainID: this.chainId }); + } + + createSmartContractController(abi?: AbiRegistry): SmartContractController { + return new SmartContractController({ chainID: this.chainId, networkProvider: this.networkProvider, abi }); + } + + createTokenManagementController(): TokenManagementController { + return new TokenManagementController({ chainID: this.chainId, networkProvider: this.networkProvider }); + } + + createTransfersController(): TransfersController { + return new TransfersController({ chainID: this.chainId }); + } +} + +export class TestnetEntrypoint extends NetworkEntrypoint { + constructor(url?: string, kind?: string) { + const entrypointConfig = new TestnetEntrypointConfig(); + super( + url || entrypointConfig.networkProviderUrl, + kind || entrypointConfig.networkProviderKind, + entrypointConfig.chainId, + ); + } +} + +export class DevnetEntrypoint extends NetworkEntrypoint { + constructor(url?: string, kind?: string) { + const entrypointConfig = new DevnetEntrypointConfig(); + super( + url || entrypointConfig.networkProviderUrl, + kind || entrypointConfig.networkProviderKind, + entrypointConfig.chainId, + ); + } +} + +export class MainnetEntrypoint extends NetworkEntrypoint { + constructor(url?: string, kind?: string) { + const entrypointConfig = new MainnetEntrypointConfig(); + super( + url || entrypointConfig.networkProviderUrl, + kind || entrypointConfig.networkProviderKind, + entrypointConfig.chainId, + ); + } +} diff --git a/src/entrypoints/index.ts b/src/entrypoints/index.ts new file mode 100644 index 00000000..311ba213 --- /dev/null +++ b/src/entrypoints/index.ts @@ -0,0 +1,2 @@ +export * from "./config"; +export * from "./entrypoints"; diff --git a/src/errors.ts b/src/errors.ts index a0a47f0a..67878c12 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -448,3 +448,12 @@ export class ErrContractQuery extends Err { super(originalError.message.replace("executeQuery:", "")); } } + +/** + * Signals that the network provider provided is not valid + */ +export class ErrInvalidNetworkProviderKind extends Err { + public constructor() { + super("Invalid network provider kind. Choose between `api` and `proxy`."); + } +} diff --git a/src/index.ts b/src/index.ts index 7207227d..2142dde9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,6 +15,7 @@ export * from "./asyncTimer"; export * from "./config"; export * from "./converters"; export * from "./delegation"; +export * from "./entrypoints"; export * from "./errors"; export * from "./gasEstimator"; export * from "./interface"; diff --git a/src/message.ts b/src/message.ts index 05c047ab..8b2dbfce 100644 --- a/src/message.ts +++ b/src/message.ts @@ -1,6 +1,5 @@ -import { IAddress } from "./interface"; -import { DEFAULT_MESSAGE_VERSION, MESSAGE_PREFIX, SDK_JS_SIGNER, UNKNOWN_SIGNER } from "./constants"; import { Address } from "./address"; +import { DEFAULT_MESSAGE_VERSION, MESSAGE_PREFIX, SDK_JS_SIGNER, UNKNOWN_SIGNER } from "./constants"; const createKeccakHash = require("keccak"); @@ -16,7 +15,7 @@ export class Message { /** * Address of the wallet that performed the signing operation. */ - public address?: IAddress; + public address?: Address; /** * Number representing the message version. */ @@ -29,7 +28,7 @@ export class Message { constructor(options: { data: Uint8Array; signature?: Uint8Array; - address?: IAddress; + address?: Address; version?: number; signer?: string; }) { From 8882e4c8d7322dd4894b49d0edd194f143e56865 Mon Sep 17 00:00:00 2001 From: danielailie Date: Tue, 19 Nov 2024 10:32:18 +0200 Subject: [PATCH 2/3] Code review follow up --- src/entrypoints/config.ts | 71 ++++++++++++++++++++++------- src/entrypoints/entrypoints.spec.ts | 8 ++-- src/entrypoints/entrypoints.ts | 42 ++++++++--------- 3 files changed, 81 insertions(+), 40 deletions(-) diff --git a/src/entrypoints/config.ts b/src/entrypoints/config.ts index 039fbbc3..479cd1e2 100644 --- a/src/entrypoints/config.ts +++ b/src/entrypoints/config.ts @@ -4,26 +4,65 @@ export interface EntrypointConfig { chainId: string; } -export class TestnetEntrypointConfig implements EntrypointConfig { - networkProviderUrl = "https://testnet-api.multiversx.com"; - networkProviderKind = "api"; - chainId = "T"; +export class TestnetEntrypointConfig { + networkProviderUrl: string; + networkProviderKind: string; + chainId: string; + + constructor({ + networkProviderUrl = "https://testnet-api.multiversx.com", + networkProviderKind = "api", + chainId = "T", + }: Partial = {}) { + this.networkProviderUrl = networkProviderUrl; + this.networkProviderKind = networkProviderKind; + this.chainId = chainId; + } } -export class DevnetEntrypointConfig implements EntrypointConfig { - networkProviderUrl = "https://devnet-api.multiversx.com"; - networkProviderKind = "api"; - chainId = "D"; +export class DevnetEntrypointConfig { + networkProviderUrl: string; + networkProviderKind: string; + chainId: string; + constructor({ + networkProviderUrl = "https://devnet-api.multiversx.com", + networkProviderKind = "api", + chainId = "D", + }: Partial = {}) { + this.networkProviderUrl = networkProviderUrl; + this.networkProviderKind = networkProviderKind; + this.chainId = chainId; + } } -export class MainnetEntrypointConfig implements EntrypointConfig { - networkProviderUrl = "https://api.multiversx.com"; - networkProviderKind = "api"; - chainId = "1"; +export class MainnetEntrypointConfig { + networkProviderUrl: string; + networkProviderKind: string; + chainId: string; + + constructor({ + networkProviderUrl = "https://api.multiversx.com", + networkProviderKind = "api", + chainId = "1", + }: Partial = {}) { + this.networkProviderUrl = networkProviderUrl; + this.networkProviderKind = networkProviderKind; + this.chainId = chainId; + } } -export class LocalnetEntrypointConfig implements EntrypointConfig { - networkProviderUrl = "http://localhost:7950"; - networkProviderKind = "proxy"; - chainId = "localnet"; +export class LocalnetEntrypointConfig { + networkProviderUrl: string; + networkProviderKind: string; + chainId: string; + + constructor({ + networkProviderUrl = "http://localhost:7950", + networkProviderKind = "proxy", + chainId = "localnet", + }: Partial = {}) { + this.networkProviderUrl = networkProviderUrl; + this.networkProviderKind = networkProviderKind; + this.chainId = chainId; + } } diff --git a/src/entrypoints/entrypoints.spec.ts b/src/entrypoints/entrypoints.spec.ts index 43937c65..d088e395 100644 --- a/src/entrypoints/entrypoints.spec.ts +++ b/src/entrypoints/entrypoints.spec.ts @@ -42,7 +42,7 @@ describe("TestEntrypoint", () => { this.timeout(30000); const abi = await loadAbiRegistry("src/testdata/adder.abi.json"); const sender = Account.newFromPem(alicePem.pemFileText); - const accountAddress = new Address(sender.address.bech32()); + const accountAddress = new Address(sender.address); sender.nonce = await entrypoint.recallAccountNonce(accountAddress); const controller = entrypoint.createSmartContractController(abi); @@ -105,7 +105,7 @@ describe("TestEntrypoint", () => { transaction.signature = await sender.sign(txComputer.computeBytesForSigning(transaction)); const relayedController = entrypoint.createRelayedController(); - const relayedTransaction = relayedController.createRelayedV2Transaction( + const relayedTransaction = await relayedController.createRelayedV2Transaction( relayer, BigInt(relayer.getNonceThenIncrement().valueOf()), { @@ -114,6 +114,8 @@ describe("TestEntrypoint", () => { }, ); - assert.equal((await relayedTransaction).chainID, "D"); + assert.equal(relayedTransaction.chainID, "D"); + assert.deepEqual(transaction.data, Buffer.from("hello")); + assert.equal(relayedTransaction.gasLimit, 0n); }); }); diff --git a/src/entrypoints/entrypoints.ts b/src/entrypoints/entrypoints.ts index 06379dfb..4dc8adbd 100644 --- a/src/entrypoints/entrypoints.ts +++ b/src/entrypoints/entrypoints.ts @@ -20,16 +20,16 @@ class NetworkEntrypoint { private networkProvider: ApiNetworkProvider | ProxyNetworkProvider; private chainId: string; - constructor(networkProviderUrl: string, networkProviderKind: string, chainId: string) { - if (networkProviderKind === "proxy") { - this.networkProvider = new ProxyNetworkProvider(networkProviderUrl); - } else if (networkProviderKind === "api") { - this.networkProvider = new ApiNetworkProvider(networkProviderUrl); + constructor(options: { networkProviderUrl: string; networkProviderKind: string; chainId: string }) { + if (options.networkProviderKind === "proxy") { + this.networkProvider = new ProxyNetworkProvider(options.networkProviderUrl); + } else if (options.networkProviderKind === "api") { + this.networkProvider = new ApiNetworkProvider(options.networkProviderUrl); } else { throw new ErrInvalidNetworkProviderKind(); } - this.chainId = chainId; + this.chainId = options.chainId; } async signTransaction(transaction: Transaction, account: IAccount): Promise { @@ -111,32 +111,32 @@ class NetworkEntrypoint { export class TestnetEntrypoint extends NetworkEntrypoint { constructor(url?: string, kind?: string) { const entrypointConfig = new TestnetEntrypointConfig(); - super( - url || entrypointConfig.networkProviderUrl, - kind || entrypointConfig.networkProviderKind, - entrypointConfig.chainId, - ); + super({ + networkProviderUrl: url || entrypointConfig.networkProviderUrl, + networkProviderKind: kind || entrypointConfig.networkProviderKind, + chainId: entrypointConfig.chainId, + }); } } export class DevnetEntrypoint extends NetworkEntrypoint { constructor(url?: string, kind?: string) { const entrypointConfig = new DevnetEntrypointConfig(); - super( - url || entrypointConfig.networkProviderUrl, - kind || entrypointConfig.networkProviderKind, - entrypointConfig.chainId, - ); + super({ + networkProviderUrl: url || entrypointConfig.networkProviderUrl, + networkProviderKind: kind || entrypointConfig.networkProviderKind, + chainId: entrypointConfig.chainId, + }); } } export class MainnetEntrypoint extends NetworkEntrypoint { constructor(url?: string, kind?: string) { const entrypointConfig = new MainnetEntrypointConfig(); - super( - url || entrypointConfig.networkProviderUrl, - kind || entrypointConfig.networkProviderKind, - entrypointConfig.chainId, - ); + super({ + networkProviderUrl: url || entrypointConfig.networkProviderUrl, + networkProviderKind: kind || entrypointConfig.networkProviderKind, + chainId: entrypointConfig.chainId, + }); } } From dd67b79258de14848cc15f16fd9a480a54d9f5da Mon Sep 17 00:00:00 2001 From: danielailie Date: Tue, 19 Nov 2024 10:54:30 +0200 Subject: [PATCH 3/3] Fix test for relayed controller --- src/entrypoints/entrypoints.spec.ts | 10 +++++++--- src/relayed/relayedController.ts | 1 - 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/entrypoints/entrypoints.spec.ts b/src/entrypoints/entrypoints.spec.ts index d088e395..42b2241e 100644 --- a/src/entrypoints/entrypoints.spec.ts +++ b/src/entrypoints/entrypoints.spec.ts @@ -113,9 +113,13 @@ describe("TestEntrypoint", () => { innerTransactionGasLimit, }, ); - assert.equal(relayedTransaction.chainID, "D"); - assert.deepEqual(transaction.data, Buffer.from("hello")); - assert.equal(relayedTransaction.gasLimit, 0n); + assert.deepEqual( + relayedTransaction.data, + Buffer.from( + "relayedTxV2@0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1@012fd1@68656c6c6f@c1eed3ac766d6b94aa53a1348d38eac8db60be0a1b2d0873247b61b8b25bbcb45bf9c1518227bcadd5044d4c027bdb935e0164243b2b2df9a5b250a10aca260e", + ), + ); + assert.equal(relayedTransaction.gasLimit, 442000n); }); }); diff --git a/src/relayed/relayedController.ts b/src/relayed/relayedController.ts index 3a219c47..558cbb95 100644 --- a/src/relayed/relayedController.ts +++ b/src/relayed/relayedController.ts @@ -41,7 +41,6 @@ export class RelayedController { const transaction = this.factory.createRelayedV2Transaction(sender.address, options); transaction.nonce = nonce; - transaction.gasLimit = BigInt(0); transaction.signature = await sender.sign(this.txComputer.computeBytesForSigning(transaction)); return transaction;