Skip to content

Commit

Permalink
Allow for autoStart and restarModelOnDeploy (#96)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
moshmage authored Apr 4, 2023
1 parent 75beb97 commit 05c1980
Show file tree
Hide file tree
Showing 10 changed files with 150 additions and 28 deletions.
53 changes: 39 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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

Expand All @@ -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
Expand Down
33 changes: 28 additions & 5 deletions src/base/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,38 @@ import {noop} from '@utils/noop';

export class Model<Methods = any> {
protected _contract!: Web3Contract<Methods>;
protected _contractAddress?: string;
private readonly web3Connection!: Web3Connection;

/**
* Returns the {@link Web3Contract} class representing this contract
*/
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
Expand All @@ -48,14 +63,16 @@ export class Model<Methods = any> {
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() {
Expand Down Expand Up @@ -142,9 +159,15 @@ export class Model<Methods = any> {

/**
* 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;
})
}
}
14 changes: 10 additions & 4 deletions src/base/web3-connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand All @@ -24,7 +30,7 @@ export class Web3Connection {
get Account(): Account { return this.account; }

async getAddress(): Promise<string> {
return this.account ? this.account.address :
return this.account ? this.account.address :
(await this.eth?.givenProvider?.request({method: 'eth_requestAccounts'}) || [""])[0];
}

Expand Down Expand Up @@ -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);

Expand Down
15 changes: 15 additions & 0 deletions src/interfaces/web3-connection-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import {PromiEvent, provider as Provider, TransactionReceipt} from 'web3-core';
import {Contract} from "web3-eth-contract";

export interface Web3ConnectionOptions {

/**
* Web3 Provider host
*/
web3Host?: string;

/**
* Provide a privateKey to automatically use that account when started
* If not provided, only read-mode will be possible
*/
privateKey?: string;

Expand Down Expand Up @@ -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;
}
2 changes: 1 addition & 1 deletion src/models/bounty-token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {AbiItem} from 'web3-utils';
import {nativeZeroAddress} from "@utils/constants";

export class BountyToken extends Model<BountyTokenMethods> implements Deployable {
constructor(web3Connection: Web3Connection|Web3ConnectionOptions, readonly contractAddress?: string) {
constructor(web3Connection: Web3Connection|Web3ConnectionOptions, contractAddress?: string) {
super(web3Connection, BountyTokenJson.abi as AbiItem[], contractAddress);
}

Expand Down
15 changes: 15 additions & 0 deletions src/models/erc20.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,25 @@ export class ERC20 extends Model<ERC20Methods> 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));
Expand Down
2 changes: 1 addition & 1 deletion src/models/network-v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {NetworkRegistry} from "@models/network-registry";
import BigNumber from "bignumber.js";

export class Network_v2 extends Model<Network_v2Methods> 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);
}
Expand Down
6 changes: 5 additions & 1 deletion test/base/web3-connection.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

Expand Down Expand Up @@ -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;
})
})
})
35 changes: 34 additions & 1 deletion test/models/base-model.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any>`, () => {
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`, () => {
Expand All @@ -22,4 +25,34 @@ describe(`Model<any>`, () => {
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')))
})
})
})
3 changes: 2 additions & 1 deletion test/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down

0 comments on commit 05c1980

Please sign in to comment.