From 05c19803c38b09c385e1e6a197c96b6fe13ab345 Mon Sep 17 00:00:00 2001 From: moshmage Date: Tue, 4 Apr 2023 14:55:39 +0100 Subject: [PATCH] Allow for autoStart and restarModelOnDeploy (#96) * make it cooler * make it so restartModelOnDeploy and autoStart are true by default * disable complexity for offending constructors * fix testing and default restartModelOnDeploy * base-model needs its own options * add more README.md * fix workflow badge * add `balanceOf` and deprecate `getTokenAmount` add balanceOf call to readme * deprecate transferTokenAmount and introduce `transfer` * add note to privateKey option * add note to privateKey option * add quick-start to README.md --- README.md | 53 +++++++++++++++++------ src/base/model.ts | 33 +++++++++++--- src/base/web3-connection.ts | 14 ++++-- src/interfaces/web3-connection-options.ts | 15 +++++++ src/models/bounty-token.ts | 2 +- src/models/erc20.ts | 15 +++++++ src/models/network-v2.ts | 2 +- test/base/web3-connection.spec.ts | 6 ++- test/models/base-model.spec.ts | 35 ++++++++++++++- test/utils/index.ts | 3 +- 10 files changed, 150 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index b9d0ba59..1a527769 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # dappkit A javascript SDK for web3 projects with curated community contracts to ease development and interactions with blockchain contracts. -![Build Status](https://img.shields.io/github/workflow/status/taikai/dappkit/integration-tests) +![Build Status](https://img.shields.io/github/actions/workflow/status/taikai/dappkit/integration-tests.yml) [![GitHub issues](https://img.shields.io/github/issues/taikai/dappkit)](https://GitHub.com/taikai/dappkit/issues/) ![Contributions welcome](https://img.shields.io/badge/contributions-welcome-orange.svg) [![License](https://img.shields.io/badge/license-ISC-blue.svg)](https://opensource.org/licenses/ISC) @@ -16,33 +16,51 @@ $ npm install @taikai/dappkit ## Usage ```ts -import {Web3Connection, ERC20} from '@taikai/dappkit'; +import {ERC20} from '@taikai/dappkit'; -const connection = new Web3Connection({ web3Host: process.env.WEB3_HOST_PROVIDER }); +const erc20 = new ERC20({ web3Host: process.env.WEB3_HOST_PROVIDER }); -await connection.start(); // start web3 connection so assignments are made -await connection.connect(); // connect web3 by asking the user to allow the connection (this is needed for the user to _interact_ with the chain) - -const erc20Deployer = new ERC20(connection); -await erc20Deployer.loadAbi(); // load abi contract is only needed for deploy actions +await erc20.connect(); // connect web3 by asking the user to allow the connection and interact with the chain const tx = await erc20Deployer.deployJsonAbi( 'Token Name', // the name of the token '$tokenSymbol', // the symbol of the token "1000000000000000000000000", // the total amount of the token (with 18 decimals; 1M = 1000000000000000000000000) - await erc20Deployer.connection.getAddress() // the owner of the total amount of the tokens (your address) + "0xOwnerOfErc20Address" // the owner of the total amount of the tokens (your address) ); -console.log(tx); // { ... , contractAddress: string} +await erc20.transfer('0xYourOtherAddress', 1); // transfer 1 token from your address to other address +console.log(await erc20.balanceOf('0xYourOtherAddress')) // 1 +``` + +### Just want to start a connection? + +```ts +import {Web3Connection} from '@taikai/dappkit'; -const myToken = new ERC20(connection, tx.contractAddress); +const web3Connection = new Web3Connection({web3Host: 'https://rpc.tld'}); -await myToken.start() // load contract and connection into the class representing your token -await myToken.transferTokenAmount('0xYourOtherAddress', 1); // transfer 1 token from your address to other address +await web3Connection.connect(); +console.log(`Address`, await web3Connection.getAddress()); +``` + +### Server side? + +```ts +import {Web3Connection, Web3ConnectionOptions} from '@taikai/dappkit'; + +const web3ConnecitonOptions: Web3ConnectionOptions = { + web3Host: 'https://rpc.tld', + // no need to provide privateKey for read-only + privateKey: 'your-private-key', // never share your private key +} + +const web3Connection = new Web3Connection(web3ConnecitonOptions); + +console.log(`Address`, await web3Connection.getAddress()); ``` -Please refer to the [`test/`](./test/models) folder to read further usage examples of the various contracts available. ## Documentation @@ -51,6 +69,13 @@ Please refer to the [`test/`](./test/models) folder to read further usage exampl * [SDK Documentation](https://sdk.dappkit.dev/) * [Use Cases](https://docs.dappkit.dev/sdk-documentation/use-cases) +Please refer to the [`test/`](./test/models) folder to read further usage examples of the various contracts available. + +## Quick start +- [Node JS](https://stackblitz.com/edit/node-b3cgaa?file=index.js) +- [NextJs](https://stackblitz.com/edit/nextjs-nzulwe?file=pages/index.js) +- [Angular](https://github.com/taikai/dappkit-testflight) + ### How to Generate Documentation You can generate the documentation locally by issuing diff --git a/src/base/model.ts b/src/base/model.ts index efa44b1a..ab0d8878 100644 --- a/src/base/model.ts +++ b/src/base/model.ts @@ -13,6 +13,7 @@ import {noop} from '@utils/noop'; export class Model { protected _contract!: Web3Contract; + protected _contractAddress?: string; private readonly web3Connection!: Web3Connection; /** @@ -20,16 +21,30 @@ export class Model { */ get contract() { return this._contract; } + /** + * Returns the {@link _contractAddress} string representing this models contract address (if any) + */ + get contractAddress() { return this._contractAddress; } + + + /* eslint-disable complexity */ constructor(web3Connection: Web3Connection | Web3ConnectionOptions, readonly abi: AbiItem[], - readonly contractAddress?: string) { + contractAddress?: string) { if (!abi || !abi.length) throw new Error(Errors.MissingAbiInterfaceFromArguments); + if (contractAddress) + this._contractAddress = contractAddress; + if (web3Connection instanceof Web3Connection) this.web3Connection = web3Connection; else this.web3Connection = new Web3Connection(web3Connection); + + if (this.web3Connection.started) + this.loadAbi(); } + /* eslint-enable complexity */ /** * Pointer to the {@link Web3Connection} assigned to this contract class @@ -48,14 +63,16 @@ export class Model { get account(): Account { return this.connection.Account; } /** - * Permissive way of initializing the contract, used primarily for deploys. Prefer to use {@link loadContract} + * Permissive way of initializing the contract, used primarily for deploys and options.autoStart = true + * Prefer to use {@link loadContract} */ loadAbi() { this._contract = new Web3Contract(this.web3, this.abi, this.contractAddress); } /** - * Preferred way of initializing and loading a contract + * Preferred way of initializing and loading a contract, use this function to customize contract loading, + * initializing any other dependencies the contract might have when extending from Model * @throws Errors.MissingContractAddress */ loadContract() { @@ -142,9 +159,15 @@ export class Model { /** * Deploy the loaded abi contract - * @protected */ - protected async deploy(deployOptions: DeployOptions, account?: Account) { + async deploy(deployOptions: DeployOptions, account?: Account) { return this.contract.deploy(this.abi, deployOptions, account) + .then(tx => { + if (this.web3Connection.options.restartModelOnDeploy && tx.contractAddress) { + this._contractAddress = tx.contractAddress; + this.loadContract(); + } + return tx; + }) } } diff --git a/src/base/web3-connection.ts b/src/base/web3-connection.ts index 033f6f85..041b6a8c 100644 --- a/src/base/web3-connection.ts +++ b/src/base/web3-connection.ts @@ -10,12 +10,18 @@ export class Web3Connection { protected web3!: Web3; protected account!: Account; + /* eslint-disable complexity */ constructor(readonly options: Web3ConnectionOptions) { - const {web3CustomProvider: provider = null} = options; - if (provider && typeof provider !== "string" && provider?.connected) { + const {web3CustomProvider: provider = null, autoStart = true} = options; + + if (autoStart || (provider && typeof provider !== "string" && provider?.connected)) { this.start(); } + + if (options.restartModelOnDeploy === undefined) + this.options.restartModelOnDeploy = true; } + /* eslint-enable complexity */ get started() { return !!this.web3; } get eth(): Eth { return this.web3?.eth; } @@ -24,7 +30,7 @@ export class Web3Connection { get Account(): Account { return this.account; } async getAddress(): Promise { - return this.account ? this.account.address : + return this.account ? this.account.address : (await this.eth?.givenProvider?.request({method: 'eth_requestAccounts'}) || [""])[0]; } @@ -96,7 +102,7 @@ export class Web3Connection { throw new Error(Errors.ProviderOptionsAreMandatoryIfIPC); provider = new Web3.providers.IpcProvider(web3Link, web3ProviderOptions); } - + if (!provider) throw new Error(Errors.FailedToAssignAProvider); diff --git a/src/interfaces/web3-connection-options.ts b/src/interfaces/web3-connection-options.ts index b90c1741..ce8b3b19 100644 --- a/src/interfaces/web3-connection-options.ts +++ b/src/interfaces/web3-connection-options.ts @@ -3,6 +3,7 @@ import {PromiEvent, provider as Provider, TransactionReceipt} from 'web3-core'; import {Contract} from "web3-eth-contract"; export interface Web3ConnectionOptions { + /** * Web3 Provider host */ @@ -10,6 +11,7 @@ export interface Web3ConnectionOptions { /** * Provide a privateKey to automatically use that account when started + * If not provided, only read-mode will be possible */ privateKey?: string; @@ -40,4 +42,17 @@ export interface Web3ConnectionOptions { resolve: (data: any) => void, reject: (e: unknown) => void, debug?: boolean) => void; + + /** + * If true, web3Connection will call `.start()` on construction + * @default true + */ + autoStart?: boolean; + + /** + * If true, model will call .loadContract() after being deployed with the returned contractAddress + * from the transaction receipt + * @default true + */ + restartModelOnDeploy?: boolean; } diff --git a/src/models/bounty-token.ts b/src/models/bounty-token.ts index 77e7340a..e7922bf7 100644 --- a/src/models/bounty-token.ts +++ b/src/models/bounty-token.ts @@ -11,7 +11,7 @@ import {AbiItem} from 'web3-utils'; import {nativeZeroAddress} from "@utils/constants"; export class BountyToken extends Model implements Deployable { - constructor(web3Connection: Web3Connection|Web3ConnectionOptions, readonly contractAddress?: string) { + constructor(web3Connection: Web3Connection|Web3ConnectionOptions, contractAddress?: string) { super(web3Connection, BountyTokenJson.abi as AbiItem[], contractAddress); } diff --git a/src/models/erc20.ts b/src/models/erc20.ts index a77f65e7..9250dbe4 100644 --- a/src/models/erc20.ts +++ b/src/models/erc20.ts @@ -45,10 +45,25 @@ export class ERC20 extends Model implements Deployable { return fromDecimals(await this.callTx(this.contract.methods.totalSupply()), this.decimals); } + async balanceOf(address: string) { + return fromSmartContractDecimals(await this.callTx(this.contract.methods.balanceOf(address)), this.decimals); + } + + /** + * @deprecated + */ async getTokenAmount(address: string) { return fromSmartContractDecimals(await this.callTx(this.contract.methods.balanceOf(address)), this.decimals); } + async transfer(toAddress: string, amount: string|number) { + const tokenAmount = toSmartContractDecimals(amount, this.decimals); + return this.sendTx(this.contract.methods.transfer(toAddress, tokenAmount)); + } + + /** + * @deprecated + */ async transferTokenAmount(toAddress: string, amount: string | number) { const tokenAmount = toSmartContractDecimals(amount, this.decimals); return this.sendTx(this.contract.methods.transfer(toAddress, tokenAmount)); diff --git a/src/models/network-v2.ts b/src/models/network-v2.ts index 05e1800b..57bfe57b 100644 --- a/src/models/network-v2.ts +++ b/src/models/network-v2.ts @@ -23,7 +23,7 @@ import {NetworkRegistry} from "@models/network-registry"; import BigNumber from "bignumber.js"; export class Network_v2 extends Model implements Deployable { - constructor(web3Connection: Web3Connection|Web3ConnectionOptions, readonly contractAddress?: string) { + constructor(web3Connection: Web3Connection|Web3ConnectionOptions, contractAddress?: string) { // eslint-disable-next-line @typescript-eslint/no-explicit-any super(web3Connection, (Network_v2Json as any).abi as AbiItem[], contractAddress); } diff --git a/test/base/web3-connection.spec.ts b/test/base/web3-connection.spec.ts index 0075390c..b8f17b13 100644 --- a/test/base/web3-connection.spec.ts +++ b/test/base/web3-connection.spec.ts @@ -7,7 +7,7 @@ import {getPrivateKeyFromFile} from '../utils/'; describe(`Web3Connection`, () => { it(`start() fails because missing web3host`, () => { - const web3Connection = new Web3Connection({}); + const web3Connection = new Web3Connection({autoStart: false}); expect(() => web3Connection.start()).to.throw(Errors.MissingWeb3ProviderHost); }); @@ -39,5 +39,9 @@ describe(`Web3Connection`, () => { it(`get ETHNetworkId`, async () => { expect(await web3Connection.getETHNetworkId()).to.exist; }); + + it(`Has restartModelOnDeploy as true by default`, () => { + expect(web3Connection.options.restartModelOnDeploy).to.be.true; + }) }) }) diff --git a/test/models/base-model.spec.ts b/test/models/base-model.spec.ts index dd91b125..a398a901 100644 --- a/test/models/base-model.spec.ts +++ b/test/models/base-model.spec.ts @@ -3,14 +3,17 @@ import {Web3Connection} from '@base/web3-connection'; import {Model} from '@base/model'; import {expect} from 'chai'; import {Errors} from '@interfaces/error-enum'; -import {getPrivateKeyFromFile} from '../utils/'; +import {getPrivateKeyFromFile, shouldBeRejected} from '../utils/'; +import erc20 from "../../build/contracts/ERC20.json"; describe(`Model`, () => { + let deployedAddress: string; const options: Web3ConnectionOptions = { web3Host: process.env.WEB3_HOST_PROVIDER || 'HTTP://127.0.0.1:8545', privateKey: process.env.WALLET_PRIVATE_KEY || getPrivateKeyFromFile(), skipWindowAssignment: true, + autoStart: false, } it(`throws because no Abi`, () => { @@ -22,4 +25,34 @@ describe(`Model`, () => { expect(() => new Model(web3Connection, undefined as any)) .to.throw(Errors.MissingAbiInterfaceFromArguments); }); + + it(`Does not load abi if no autoStart`, () => { + const web3Connection = new Web3Connection(options); + const model = new Model(web3Connection, erc20.abi as any); + expect(model.contract).to.be.undefined; + }); + + describe(`with autoStart: true`, () => { + it(`Starts and loads the ABI automatically and re-assigns`, async () => { + const web3Connection = new Web3Connection({...options, autoStart: true}); + const model = new Model(web3Connection, erc20.abi as any); + + const tx = + await model.deploy({data: erc20.bytecode, arguments: ["name", "symbol"]}, web3Connection.Account); + + expect(model.contract.abi).to.exist; + expect(tx.blockNumber).to.exist; + expect(tx.contractAddress).to.exist; + expect(model.contractAddress).to.be.eq(tx.contractAddress); + deployedAddress = tx.contractAddress; + + }); + + it(`Starts but can't interact, only read because no pvtkey`, async () => { + const model = new Model({...options, privateKey: undefined, autoStart: true}, erc20.abi as any, deployedAddress); + expect(await model.callTx(model.contract.methods.name())).to.be.eq('name'); + const AliceAddress = model.web3.eth.accounts.privateKeyToAccount(getPrivateKeyFromFile(1)).address; + await shouldBeRejected(model.sendTx(model.contract.methods.transfer(AliceAddress, '10'))) + }) + }) }) diff --git a/test/utils/index.ts b/test/utils/index.ts index 5d97bf9b..037b200b 100644 --- a/test/utils/index.ts +++ b/test/utils/index.ts @@ -30,7 +30,8 @@ export async function defaultWeb3Connection(start = false, revert = false) { const options: Web3ConnectionOptions = { web3Host: process.env.WEB3_HOST_PROVIDER || 'HTTP://127.0.0.1:8545', privateKey: process.env.WALLET_PRIVATE_KEY || getPrivateKeyFromFile(), - skipWindowAssignment: true + skipWindowAssignment: true, + restartModelOnDeploy: false, } const web3Connection = new Web3Connection(options);