From 3c12a65769b9a223028df25f78abb6dde06fef35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Uhl=C3=AD=C5=99?= Date: Wed, 29 Mar 2023 13:41:44 +0200 Subject: [PATCH] feat: erc20 module (#38) Co-authored-by: Eric Mastro --- .gitignore | 1 + Readme.md | 19 +++++++ ethers/erc20.nim | 52 +++++++++++++++++ testmodule/test.nim | 1 + testmodule/testContracts.nim | 14 +---- testmodule/testErc20.nim | 96 ++++++++++++++++++++++++++++++++ testnode/contracts/TestToken.sol | 8 +++ 7 files changed, 179 insertions(+), 12 deletions(-) create mode 100644 ethers/erc20.nim create mode 100644 testmodule/testErc20.nim diff --git a/.gitignore b/.gitignore index 272d8fb..37a40a8 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ !*.* nimble.develop nimble.paths +.idea diff --git a/Readme.md b/Readme.md index 4f5e877..f0c5219 100644 --- a/Readme.md +++ b/Readme.md @@ -143,6 +143,25 @@ await subscription.unsubscribe() Subscriptions are currently only supported when using a JSON RPC provider that is created with a websockets URL such as `ws://localhost:8545`. +Utilities +--------- + +This library ships with some optional modules that provides convenience utilities for you such as: + +- `ethers/erc20` module provides you with ERC20 token implementation and its events + +Contribution +------------ + +If you want to run the tests, then before running `nimble test`, you have to +have installed NodeJS and started a testing node: + +```shell +$ cd testnode +$ npm ci +$ npm start +``` + Thanks ------ diff --git a/ethers/erc20.nim b/ethers/erc20.nim new file mode 100644 index 0000000..05fee1c --- /dev/null +++ b/ethers/erc20.nim @@ -0,0 +1,52 @@ +import pkg/stint +import pkg/ethers + +export stint +export ethers + +type + Erc20Token* = ref object of Contract + + Transfer* = object of Event + sender* {.indexed.}: Address + receiver* {.indexed.}: Address + value*: UInt256 + + Approval* = object of Event + owner* {.indexed.}: Address + spender* {.indexed.}: Address + value*: UInt256 + +method name*(erc20: Erc20Token): string {.base, contract, view.} +## Returns the name of the token. + +method symbol*(token: Erc20Token): string {.base, contract, view.} +## Returns the symbol of the token, usually a shorter version of the name. + +method decimals*(token: Erc20Token): uint8 {.base, contract, view.} +## Returns the number of decimals used to get its user representation. +## For example, if `decimals` equals `2`, a balance of `505` tokens should +## be displayed to a user as `5.05` (`505 / 10 ** 2`). + +method totalSupply*(erc20: Erc20Token): UInt256 {.base, contract, view.} +## Returns the amount of tokens in existence. + +method balanceOf*(erc20: Erc20Token, account: Address): UInt256 {.base, contract, view.} +## Returns the amount of tokens owned by `account`. + +method allowance*(erc20: Erc20Token, owner, spender: Address): UInt256 {.base, contract, view.} +## Returns the remaining number of tokens that `spender` will be allowed +## to spend on behalf of `owner` through {transferFrom}. This is zero by default. +## +## This value changes when {approve} or {transferFrom} are called. + +method transfer*(erc20: Erc20Token, recipient: Address, amount: UInt256) {.base, contract.} +## Moves `amount` tokens from the caller's account to `recipient`. + +method approve*(token: Erc20Token, spender: Address, amount: UInt256) {.base, contract.} +## Sets `amount` as the allowance of `spender` over the caller's tokens. + +method transferFrom*(erc20: Erc20Token, spender: Address, recipient: Address, amount: UInt256) {.base, contract.} +## Moves `amount` tokens from `from` to `to` using the allowance +## mechanism. `amount` is then deducted from the caller's allowance. + diff --git a/testmodule/test.nim b/testmodule/test.nim index e4da7c3..a0e4174 100644 --- a/testmodule/test.nim +++ b/testmodule/test.nim @@ -6,5 +6,6 @@ import ./testEnums import ./testEvents import ./testWallet import ./testTesting +import ./testErc20 {.warning[UnusedImport]:off.} diff --git a/testmodule/testContracts.nim b/testmodule/testContracts.nim index 20c1ec5..460d0ef 100644 --- a/testmodule/testContracts.nim +++ b/testmodule/testContracts.nim @@ -3,25 +3,15 @@ import pkg/asynctest import pkg/questionable import pkg/stint import pkg/ethers +import pkg/ethers/erc20 import ./hardhat import ./miner import ./mocks type - Erc20* = ref object of Contract - TestToken = ref object of Erc20 + TestToken = ref object of Erc20Token - Transfer = object of Event - sender {.indexed.}: Address - receiver {.indexed.}: Address - value: UInt256 - -method name*(erc20: Erc20): string {.base, contract, view.} -method totalSupply*(erc20: Erc20): UInt256 {.base, contract, view.} -method balanceOf*(erc20: Erc20, account: Address): UInt256 {.base, contract, view.} -method allowance*(erc20: Erc20, owner, spender: Address): UInt256 {.base, contract, view.} -method transfer*(erc20: Erc20, recipient: Address, amount: UInt256) {.base, contract.} method mint(token: TestToken, holder: Address, amount: UInt256): ?TransactionResponse {.base, contract.} suite "Contracts": diff --git a/testmodule/testErc20.nim b/testmodule/testErc20.nim new file mode 100644 index 0000000..d93bbc0 --- /dev/null +++ b/testmodule/testErc20.nim @@ -0,0 +1,96 @@ +import std/json +import pkg/asynctest +import pkg/questionable +import pkg/stint +import pkg/ethers +import pkg/ethers/erc20 +import ./hardhat +import ./miner +import ./mocks + +type + TestToken = ref object of Erc20Token + +method mint(token: TestToken, holder: Address, amount: UInt256): ?TransactionResponse {.base, contract.} + +suite "ERC20": + + var token, token1: Erc20Token + var testToken: TestToken + var provider: JsonRpcProvider + var snapshot: JsonNode + var accounts: seq[Address] + + setup: + provider = JsonRpcProvider.new("ws://localhost:8545") + snapshot = await provider.send("evm_snapshot") + accounts = await provider.listAccounts() + let deployment = readDeployment() + testToken = TestToken.new(!deployment.address(TestToken), provider.getSigner()) + token = Erc20Token.new(!deployment.address(TestToken), provider.getSigner()) + + teardown: + discard await provider.send("evm_revert", @[snapshot]) + + test "retrieves basic information": + check (await token.name()) == "TestToken" + check (await token.symbol()) == "TST" + check (await token.decimals()) == 12 + check (await token.totalSupply()) == 0.u256 + check (await token.balanceOf(accounts[0])) == 0.u256 + check (await token.allowance(accounts[0], accounts[1])) == 0.u256 + + test "transfer tokens": + check (await token.balanceOf(accounts[0])) == 0.u256 + check (await token.allowance(accounts[0], accounts[1])) == 0.u256 + + discard await testToken.mint(accounts[0], 100.u256) + + check (await token.totalSupply()) == 100.u256 + check (await token.balanceOf(accounts[0])) == 100.u256 + check (await token.balanceOf(accounts[1])) == 0.u256 + + await token.transfer(accounts[1], 50.u256) + + check (await token.balanceOf(accounts[0])) == 50.u256 + check (await token.balanceOf(accounts[1])) == 50.u256 + + test "approve tokens": + discard await testToken.mint(accounts[0], 100.u256) + + check (await token.allowance(accounts[0], accounts[1])) == 0.u256 + check (await token.balanceOf(accounts[0])) == 100.u256 + check (await token.balanceOf(accounts[1])) == 0.u256 + + await token.approve(accounts[1], 50.u256) + + check (await token.allowance(accounts[0], accounts[1])) == 50.u256 + check (await token.balanceOf(accounts[0])) == 100.u256 + check (await token.balanceOf(accounts[1])) == 0.u256 + + test "transferFrom tokens": + let senderAccount = accounts[0] + let receiverAccount = accounts[1] + let receiverAccountSigner = provider.getSigner(receiverAccount) + + check (await token.balanceOf(senderAccount)) == 0.u256 + check (await token.allowance(senderAccount, receiverAccount)) == 0.u256 + + discard await testToken.mint(senderAccount, 100.u256) + + check (await token.totalSupply()) == 100.u256 + check (await token.balanceOf(senderAccount)) == 100.u256 + check (await token.balanceOf(receiverAccount)) == 0.u256 + + await token.approve(receiverAccount, 50.u256) + + check (await token.allowance(senderAccount, receiverAccount)) == 50.u256 + check (await token.balanceOf(senderAccount)) == 100.u256 + check (await token.balanceOf(receiverAccount)) == 0.u256 + + await token.connect(receiverAccountSigner).transferFrom(senderAccount, receiverAccount, 50.u256) + + check (await token.balanceOf(senderAccount)) == 50.u256 + check (await token.balanceOf(receiverAccount)) == 50.u256 + check (await token.allowance(senderAccount, receiverAccount)) == 0.u256 + diff --git a/testnode/contracts/TestToken.sol b/testnode/contracts/TestToken.sol index b36fc78..1018c0d 100644 --- a/testnode/contracts/TestToken.sol +++ b/testnode/contracts/TestToken.sol @@ -6,7 +6,15 @@ import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract TestToken is ERC20 { constructor() ERC20("TestToken", "TST") {} + function decimals() public view virtual override returns (uint8) { + return 12; + } + function mint(address holder, uint amount) public { _mint(holder, amount); } + + function burn(address holder, uint amount) public { + _burn(holder, amount); + } }