diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..6fd363c --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +**/__pycache__/** +venv +package-lock.json +/ontio/* +/build +/node_modules +/.idea +/.DS_Store +/experience diff --git a/contracts/.gitignore b/contracts/.gitignore new file mode 100755 index 0000000..a2d3735 --- /dev/null +++ b/contracts/.gitignore @@ -0,0 +1,3 @@ +/venv +**/__pycache__/** +/.idea diff --git a/contracts/avm.py b/contracts/avm.py new file mode 100755 index 0000000..2a4b51d --- /dev/null +++ b/contracts/avm.py @@ -0,0 +1,19 @@ +import argparse +import binascii + + +def hexlify_avm(blob): + return binascii.hexlify(blob).decode('ascii') + + +def read_avm(filename): + with open(filename, 'rb') as f: + return hexlify_avm(f.read()) + + +if __name__ == '__main__': + args = argparse.ArgumentParser() + args.add_argument('input', metavar='I', type=str, nargs='+', help='File glob patterns to compile') + args = args.parse_args() + for avm_file in args.input: + print(read_avm(avm_file)) diff --git a/contracts/compile.py b/contracts/compile.py new file mode 100755 index 0000000..7f3c93d --- /dev/null +++ b/contracts/compile.py @@ -0,0 +1,53 @@ +import os +import argparse +from boa.compiler import Compiler +from glob import glob + +throw_conversion = bytes.fromhex( + '09f7f6f5f4f3f2f1f000f0' +) + + +def compile_contract(file, out_dir=None, export_debug=True, print_code=True): + compiler = Compiler.load(file) + basename, _ = os.path.splitext(os.path.basename(file)) + + if not out_dir: + out_dir = os.path.dirname(file) + + # if out directory doesn't exist, create the directory + if not os.path.exists(out_dir): + os.mkdir(out_dir) + + output_path = os.path.join(out_dir, '{}.avm'.format(basename)) + + avm_code = compiler.write().replace(throw_conversion, b'\xff' * len(throw_conversion)) + compiler.write_file(avm_code, output_path) + + if export_debug: + compiler.entry_module.export_debug(output_path) + + if print_code: + print('## {} ##'.format(basename)) + compiler.entry_module.to_s() + + +def compile_match_files(glob_pattern, out_dir=None, export_debug=True, print_code=True): + for file in glob(glob_pattern): + basename, ext = os.path.splitext(os.path.basename(file)) + + if ext != '.py' or basename in ('__init__', ): + continue + + compile_contract(file, out_dir, export_debug, print_code) + + +if __name__ == '__main__': + args = argparse.ArgumentParser() + args.add_argument('input', metavar='I', type=str, nargs='+', help='File glob patterns to compile') + args.add_argument('--out', '-o', type=str, help='output directory', default='build') + args.add_argument('--export-debug', action='store_true') + args = args.parse_args() + + for pattern in args.input: + compile_match_files(pattern, args.out, args.export_debug) \ No newline at end of file diff --git a/contracts/contracts/SpokkzCoin.py b/contracts/contracts/SpokkzCoin.py new file mode 100755 index 0000000..d9a8d78 --- /dev/null +++ b/contracts/contracts/SpokkzCoin.py @@ -0,0 +1,323 @@ + +from boa.interop.System.Storage import * +from boa.builtins import * + +from libs.SafeMath import * +from libs.SafeCheck import * +from libs.Utils import * + +TOKEN_NAME = 'Spokkz Token' +TOKEN_SYMBOL = 'SPKZ' + +################################################################################ +# TOKEN INFO CONSTANTS + +DEPLOYER = "AHmMXuCARpydrg8erE9FeSsoufgbYgbf1T" +INIT_SUPPLY = 1000000000 +TOKEN_DECIMALS = 8 +FACTOR = 100000000 + +################################################################################ +# STORAGE KEY CONSTANT +# Belows are storage key for some variable token information. + +OWNER_KEY = '___OWNER' +MZK_SUPPLY_KEY = '__SUPPLY' + + +################################################################################ +# STORAGE KEY PREFIX +# Since all data are stored in the key-value storage, the data need to be +# classified by key prefix. All key prefixes length must be the same. + +OWN_PREFIX = '_____own' +ALLOWANCE_PREFIX = '___allow' + + +################################################################################ +# + +def Main(operation, args): + if operation == 'Deploy': + return Deploy() + elif operation == 'Name': + return TOKEN_NAME + elif operation == 'Decimals': + return TOKEN_DECIMALS + elif operation == 'Symbol': + return TOKEN_SYMBOL + elif operation == 'TotalSupply': + return TotalSupply() + elif operation == 'BalanceOf': + if len(args) == 1: + return BalanceOf(args[0]) + elif operation == 'Transfer': + if len(args) == 3: + return Transfer(args[0], args[1], args[2]) + elif operation == 'TransferFrom': + if len(args) == 4: + return TransferFrom(args[0], args[1], args[2], args[3]) + elif operation == 'Approve': + if len(args) == 3: + return Approve(args[0], args[1], args[2]) + elif operation == 'Allowance': + if len(args) == 2: + return Allowance(args[0], args[1]) + elif operation == 'Mint': + if len(args) == 2: + return Mint(args[0], args[1]) + elif operation == 'Burn': + if len(args) == 1: + return Burn(args[0]) + elif operation == 'TransferOwnership': + if len(args) == 1: + return TransferOwnership(args[0]) + + return False + + +def Deploy(): + """ + Constructor of this contract. Only deployer hard-coded can call this function + and cannot call this function after called once. + + Followings are initialization list for this token + 1. Transfer the owner to the deployer. (Owner can mint and burn the token) + 2. Supply initial coin to the deployer. + """ + ctx = GetContext() + + # Require(CheckWitness(DEPLOYER)) # only can be initialized by deployer + Require(not Get(ctx, 'DEPLOYED')) # only can deploy once + + # disable to deploy again + Put(ctx, 'DEPLOYED', 1) + + # the first owner is the deployer + # can transfer ownership to other by calling `TransferOwner` function + Put(ctx, OWNER_KEY, DEPLOYER) + + # supply the coin. All coin will be belong to deployer. + Put(ctx, MZK_SUPPLY_KEY, INIT_SUPPLY * FACTOR) + Put(ctx, concat(OWN_PREFIX, DEPLOYER), INIT_SUPPLY * FACTOR) + + return True + + +def TotalSupply(): + """ + Gets the total supply for MZK token. The total supply can be changed by + owner's invoking function calls for minting and burning. + """ + return _totalSupply(GetContext()) + + +def BalanceOf(account): + """ + Gets the MZK token balance of an account. + + :param account: account + """ + return _balanceOf(GetContext(), account) + + +def Transfer(_from, _to, _value): + """ + Sends the amount of tokens from address `from` to address `to`. The parameter + `from` must be the invoker. + + :param _from: invoker address. + :param _to: receiver address. + :param _value: MZK amount. + """ + RequireWitness(_from) # from address validation + return _transfer(GetContext(), _from, _to, _value) + + +def TransferFrom(_originator, _from, _to, _amount): + """ + Transfers the amount of tokens in `from` address to `to` address by invoker. + Only approved amount can be sent. + + :param _originator: invoker address. + :param _from: address for withdrawing. + :param _to: address to receive. + :param _amount: MZK amount. + """ + return _transferFrom(GetContext(), _originator, _from, _to, _amount) + + +def Approve(_from, _to, _amount): + """ + Approves `to` address to withdraw MZK token from the invoker's address. It + overwrites the previous approval value. + + :param _from: invoker address. + :param _to: address to approve. + :param _amount: MZK amount to approve. + """ + RequireWitness(_from) # only the token owner can approve + return _approve(GetContext(), _from, _to, _amount) + + +def Burn(_amount): + """ + Burns the amount of MZK token from the owner's address. + :param _amount: MZK amount to burn. + """ + ctx = GetContext() + _onlyOwner(ctx) # only owner can burn the token + return _burn(ctx, Get(ctx, OWNER_KEY), _amount) + + +def Mint(_to, _amount): + """ + Mints the amount of MZK token. + :param _to: address to receive token. + :param _amount: the amount to mint. + """ + ctx = GetContext() + _onlyOwner(ctx) # only owner can mint token + return _mint(ctx, _to, _amount) + + +def TransferOwnership(_account): + """ + Transfers the ownership of this contract to other. + :param _account: address to transfer ownership. + """ + ctx = GetContext() + _onlyOwner(ctx) + return _transferOwnership(ctx, _account) + + +def Allowance(_from, _to): + """ + Gets the amount of allowance from address `from` to address `to`. + :param _from: from address + :param _to: to address + :return: the amount of allowance. + """ + return _allowance(GetContext(), _from, _to) + + +################################################################################ +# INTERNAL FUNCTIONS +# Internal functions checks parameter and storage result validation but these +# wouldn't check the witness validation, so caller function must check the +# witness if necessary. + +def _transfer(_context, _from, _to, _value): + Require(_value > 0) # transfer value must be over 0 + RequireScriptHash(_to) # to-address validation + + from_val = _accountValue(_context, _from) + to_val = _accountValue(_context, _to) + + from_val = uSub(from_val, _value) + to_val = to_val + _value + + SafePut(_context, concat(OWN_PREFIX, _from), from_val) + SafePut(_context, concat(OWN_PREFIX, _to), to_val) + + return True + + +def _balanceOf(_context, _account): + RequireScriptHash(_account) + return Get(_context, concat(OWN_PREFIX, _account)) + + +def _transferFrom(_context, _originator, _from, _to, _amount): + RequireWitness(_originator) + RequireScriptHash(_from) + RequireScriptHash(_to) + + Require(_amount > 0) + + approve_key = concat(ALLOWANCE_PREFIX, concat(_from, _originator)) + approve_amount = Get(_context, approve_key) + approve_amount = uSub(approve_amount, _amount) + + _transfer(_context, _from, _to, _amount) + SafePut(_context, approve_key, approve_amount) + + return True + + +def _approve(_context, _from, _to, _amount): + RequireScriptHash(_to) # to-address validation + Require(_amount >= 0) # amount must be not minus value + + from_val = _accountValue(_context, _from) + + Require(from_val >= _amount) # the token owner must have the amount over approved + + approve_key = concat(ALLOWANCE_PREFIX, concat(_from, _to)) + SafePut(_context, approve_key, _amount) + + return True + + +def _burn(_context, _account, _amount): + Require(_amount > 0) # the amount to burn should be over 0 + + account_val = _balanceOf(_context, _account) + total_supply = _totalSupply(_context) + + Require(_amount < total_supply) # should be not over total supply + + # burn the token from account. It also subtract the total supply + account_val = uSub(account_val, _amount) + total_supply = uSub(total_supply, _amount) + + SafePut(_context, concat(OWN_PREFIX, _account), account_val) + SafePut(_context, MZK_SUPPLY_KEY, total_supply) + return True + + +def _mint(_context, _to, _amount): + Require(_amount > 0) # mint value must be over 0 + RequireScriptHash(_to) # to address should + + total_supply = _totalSupply(_context) + to_val = _accountValue(_context, _to) + + # Add total supply value and give the token to the to-address + total_supply += _amount + to_val += _amount + + SafePut(_context, MZK_SUPPLY_KEY, total_supply) + SafePut(_context, concat(OWN_PREFIX, _to), to_val) + return True + + +def _transferOwnership(_context, _account): + RequireScriptHash(_account) + Put(_context, OWNER_KEY, _account) + + +################################################################################ +# modifiers + +def _onlyOwner(_context): + """ + Checks the invoker is the contract owner or not. Owner key is saved in the + storage key `___OWNER`, so check its value and invoker. + """ + RequireWitness(Get(_context, OWNER_KEY)) + + +################################################################################ +# + +def _accountValue(_context, _account): + return Get(_context, concat(OWN_PREFIX, _account)) + + +def _totalSupply(_context): + return Get(_context, MZK_SUPPLY_KEY) + + +def _allowance(_context, _from, _to): + return Get(_context, concat(ALLOWANCE_PREFIX, concat(_from, _to))) diff --git a/contracts/libs/SafeCheck.py b/contracts/libs/SafeCheck.py new file mode 100755 index 0000000..28c6f35 --- /dev/null +++ b/contracts/libs/SafeCheck.py @@ -0,0 +1,38 @@ + +from libs.Utils import Revert +from boa.interop.System.Runtime import CheckWitness + + +def Require(condition): + """ + If not satisfying the condition, revert the transaction. All + changed storage will be rolled back. + :param condition: required condition. + :return: True if satisfying the condition. + """ + if not condition: + Revert() + return True + + +def RequireScriptHash(key): + """ + Checks the bytearray parameter is script hash or not. Script Hash + length should be equal to 20. + :param key: bytearray parameter to check script hash format. + :return: True if script hash or revert the transaction. + """ + Require(len(key) == 20) + return True + + +def RequireWitness(witness): + """ + Checks the transaction sender is equal to the witness. If not + satisfying, revert the transaction. + :param witness: required transaction sender + :return: True if transaction sender or revert the transaction. + """ + Require(CheckWitness(witness)) + return True + diff --git a/contracts/libs/SafeMath.py b/contracts/libs/SafeMath.py new file mode 100755 index 0000000..a08d4bc --- /dev/null +++ b/contracts/libs/SafeMath.py @@ -0,0 +1,18 @@ + +""" + SafeMath produces elementary operations such as plus, minus but + check whether it is safe and revert the transaction if it is not safe. +""" + +from libs.SafeCheck import Require + + +def uSub(a, b): + """ + Operates a minus b with condition that a - b can never be below 0. + :param a: operand a + :param b: operand b + :return: a - b if a - b > 0 or revert the transaction. + """ + Require(a >= b) + return a - b diff --git a/contracts/libs/Utils.py b/contracts/libs/Utils.py new file mode 100755 index 0000000..cdbf0b0 --- /dev/null +++ b/contracts/libs/Utils.py @@ -0,0 +1,18 @@ + +from boa.interop.System.Storage import Put, Delete + + +def Revert(): + """ + Revert the transaction. The opcodes of this function is `09f7f6f5f4f3f2f1f000f0`, + but it will be changed to `ffffffffffffffffffffff` since opcode THROW doesn't + work, so, revert by calling unused opcode. + """ + raise Exception(0xF0F1F2F3F4F5F6F7) + + +def SafePut(context, key, value): + if value == 0: + Delete(context, key) + else: + Put(context, key, value) diff --git a/contracts/libs/__init__.py b/contracts/libs/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/contracts/requirements.txt b/contracts/requirements.txt new file mode 100755 index 0000000..73dd7aa --- /dev/null +++ b/contracts/requirements.txt @@ -0,0 +1,11 @@ +aenum==2.1.2 +astor==0.7.1 +colorama==0.3.9 +coz-bytecode==0.5.1 +logzero==1.5.0 +git+https://github.com/ontio/neo-boa.git +pycryptodomex +cryptography +ecdsa +base58 +requests diff --git a/index.ts b/index.ts new file mode 100755 index 0000000..04bca77 --- /dev/null +++ b/index.ts @@ -0,0 +1 @@ +export * from './utils'; diff --git a/migrations/01_deploy_token.ts b/migrations/01_deploy_token.ts new file mode 100755 index 0000000..c04b398 --- /dev/null +++ b/migrations/01_deploy_token.ts @@ -0,0 +1,11 @@ +import { Deployer } from "../utils/deployer"; +import { utils } from 'ontology-ts-sdk'; + +module.exports = async (deployer: Deployer) => { + const contract = await deployer.deploy('TestContract', 'Deploy'); + contract.deployed() + .then(async (aa) => { + console.log('contract hash', contract.codeHash); + console.log(await deployer.client.getStorage(contract.codeHash, utils.str2hexstr('test'))); + }); +}; \ No newline at end of file diff --git a/ontology.json b/ontology.json new file mode 100755 index 0000000..1990182 --- /dev/null +++ b/ontology.json @@ -0,0 +1,30 @@ +{ + "avmFiles": "build/**/*.avm", + "testNetwork": "local", + "network": { + "local": { + "method": "rpc", + "host": "http://127.0.0.1:20336", + "wallet": { + "file": "wallet/wallet_local.dat", + "password": "password" + } + }, + + "testnet": { + "method": "rpc", + "host": "http://polaris1.ont.io:20336", + "wallet": { + "address": "AazEvfQPcQ2GEFFPLF1ZLwQ7K5jDn81hve", + "privateKey": "75de8489fcb2dcaf2ef3cd607feffde18789de7da129b5e97c81e001793cb7cf" + } + }, + + "mainnet": { + "method": "rpc", + "host": "http://dappnode1.ont.io", + "port": "20336", + "wallet": {} + } + } +} diff --git a/package.json b/package.json new file mode 100755 index 0000000..2145e15 --- /dev/null +++ b/package.json @@ -0,0 +1,36 @@ +{ + "name": "spokkz-ontology-smart-contracts", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "init": "npm run init:python", + "init:python": "cd contracts && virtualenv venv && . venv/*/activate && pip3 install -r requirements.txt", + "clean": "npm run clean:python && npm run clean:build", + "clean:python": "cd contracts && rm -rf venv", + "clean:build": "rm -rf build", + "build": "cd contracts && . venv/*/activate && python3 compile.py -o ../build ./contracts/*.py", + "rebuild": "npm run clean && npm run init && npm run build", + "deploy:local": "npm run rebuild && ts-node utils/cli.ts --mode local --deploy", + "test": "npm run build && mocha -r ts-node/register ./test/**/*.ts --timeout 60000" + }, + "author": "", + "license": "MIT", + "devDependencies": { + "@types/chai": "^4.1.4", + "@types/command-line-args": "^5.0.0", + "@types/glob": "^5.0.35", + "@types/mocha": "^5.2.5", + "@types/node": "^10.9.2", + "chai": "^4.1.2", + "command-line-args": "^5.0.2", + "mocha": "^5.2.0", + "ts-node": "^7.0.1", + "typescript": "^3.0.1" + }, + "dependencies": { + "chalk": "^2.4.1", + "glob": "^7.1.2", + "ontology-ts-sdk": "^0.9.3" + } +} diff --git a/test/spokkz-token.spec.ts b/test/spokkz-token.spec.ts new file mode 100755 index 0000000..e22d191 --- /dev/null +++ b/test/spokkz-token.spec.ts @@ -0,0 +1,658 @@ +import { BigNumber } from 'bignumber.js'; +import { + Crypto, + Parameter, + ParameterType, + RestClient, + RpcClient, + TransactionBuilder, + utils, + WebsocketClient +} from 'ontology-ts-sdk'; +import { DeployedTransaction, TestDeployer } from "../utils/deployer"; +import { waitForTransactionReceipt } from "../utils/transaction"; +import chai from 'chai'; +import { num2ByteArray } from "../utils/big-number"; + +chai.should(); + +describe('SpokkzCoin Contract', () => { + const privateKey = TestDeployer.forSign.privateKey; + const address = TestDeployer.forSign.address; + + let contract: DeployedTransaction; + let client: RestClient | RpcClient | WebsocketClient; + + let randomAccount: { privateKey: Crypto.PrivateKey, address: Crypto.Address }[] = []; + + let isDeployed: () => Promise; + let getOwner: () => Promise; + let totalSupply: () => Promise; + + let getBalance: ( + address: Crypto.Address + ) => Promise; + + let allowance: ( + _from: Crypto.Address, + _to: Crypto.Address + ) => Promise; + + let approve: ( + _from: Crypto.Address, + _to: Crypto.Address, + _amount: BigNumber | string, + privateKey: Crypto.PrivateKey, + payer: Crypto.Address + ) => Promise; + + let transfer: ( + _from: Crypto.Address, + _to: Crypto.Address, + _amount: BigNumber | string, + privateKey: Crypto.PrivateKey, + payer?: Crypto.Address + ) => Promise; + + let transferFrom: ( + _originator: Crypto.Address, + _from: Crypto.Address, + _to: Crypto.Address, + _amount: string | BigNumber, + privateKey: Crypto.PrivateKey, + payer?: Crypto.Address + ) => Promise; + + let burn: ( + _amount: BigNumber | string, + privateKey: Crypto.PrivateKey, + payer?: Crypto.Address + ) => Promise; + + let mint: ( + _to: Crypto.Address, + _amount: BigNumber | string, + privateKey: Crypto.PrivateKey, + payer: Crypto.Address + ) => Promise; + + let transferOwnership: ( + address: Crypto.Address, + privateKey: Crypto.PrivateKey, + payer: Crypto.Address + ) => Promise; + + before (async () => { + // deploy the contract. + contract = await TestDeployer.deploy('SpokkzCoin'); + await contract.deployed(); + client = TestDeployer.client; + + isDeployed = async () => { + const key = utils.str2hexstr('DEPLOYED'); + const value = (await client.getStorage(contract.codeHash, key)).result; + return (value !== null); + }; + + getOwner = async () => { + const key = utils.str2hexstr('___OWNER'); + const hexAddress = (await client.getStorage(contract.codeHash, key)).result; + return Crypto.Address.deserialize(new utils.StringReader(hexAddress)); + }; + + transferOwnership = async ( + address: Crypto.Address, + privateKey: Crypto.PrivateKey, + payer: Crypto.Address + ) => { + const tx = TransactionBuilder.makeInvokeTransaction( + 'TransferOwnership', + [ + new Parameter('_account', ParameterType.ByteArray, address.serialize()) + ], + contract.address, + '0', + '20000', + payer + ); + + TransactionBuilder.signTransaction(tx, privateKey); + const txHash = (await client.sendRawTransaction(tx.serialize())).result; + await waitForTransactionReceipt(client, txHash); + }; + + totalSupply = async () => { + const hexValue = (await client.getStorage(contract.codeHash, utils.str2hexstr('__SUPPLY'))).result; + return new BigNumber((hexValue === null) ? '0' : utils.reverseHex(hexValue), 16); + }; + + getBalance = async ( + address: Crypto.Address + ) => { + const key = utils.str2hexstr('_____own') + address.serialize(); + const balance = (await client.getStorage(contract.codeHash, key)).result; + return new BigNumber((typeof balance === 'string') ? utils.reverseHex(balance) : '00', 16); + }; + + approve = async ( + _from: Crypto.Address, + _to: Crypto.Address, + _amount: BigNumber | string, + privateKey: Crypto.PrivateKey, + payer: Crypto.Address + ) => { + const tx = TransactionBuilder.makeInvokeTransaction( + 'Approve', + [ + new Parameter('_from', ParameterType.ByteArray, _from.serialize()), + new Parameter('_to', ParameterType.ByteArray, _to.serialize()), + new Parameter('_amount', ParameterType.ByteArray, num2ByteArray(_amount)) + ], + contract.address, + '0', + '20000', + payer + ); + + TransactionBuilder.signTransaction(tx, privateKey); + const txHash = (await client.sendRawTransaction(tx.serialize())).result; + await waitForTransactionReceipt(client, txHash); + }; + + allowance = async ( + _from: Crypto.Address, + _to: Crypto.Address + ) => { + const key = utils.str2hexstr('___allow') + _from.serialize() + _to.serialize(); + const allowed = (await client.getStorage(contract.codeHash, key)).result; + + return (allowed === null) ? new BigNumber(0) : new BigNumber(utils.reverseHex(allowed), 16); + }; + + transfer = async ( + _from: Crypto.Address, + _to: Crypto.Address, + _amount: BigNumber | string, + privateKey: Crypto.PrivateKey, + payer?: Crypto.Address + ) => { + const tx = TransactionBuilder.makeInvokeTransaction( + 'Transfer', + [ + new Parameter('_from', ParameterType.ByteArray, _from.serialize()), + new Parameter('_to', ParameterType.ByteArray, _to.serialize()), + new Parameter('_amount', ParameterType.ByteArray, num2ByteArray(_amount)) + ], + contract.address, + '0', + '20000', + payer || _from); + + TransactionBuilder.signTransaction(tx, privateKey); + const txHash = (await client.sendRawTransaction(tx.serialize())).result; + await waitForTransactionReceipt(client, txHash); + }; + + transferFrom = async ( + _originator: Crypto.Address, + _from: Crypto.Address, + _to: Crypto.Address, + _amount: string | BigNumber, + privateKey: Crypto.PrivateKey, + payer?: Crypto.Address + ) => { + const tx = TransactionBuilder.makeInvokeTransaction( + 'TransferFrom', + [ + new Parameter('_originator', ParameterType.ByteArray, _originator.serialize()), + new Parameter('_from', ParameterType.ByteArray, _from.serialize()), + new Parameter('_to', ParameterType.ByteArray, _to.serialize()), + new Parameter('_amount', ParameterType.ByteArray, num2ByteArray(_amount)) + ], + contract.address, + '0', + '20000', + payer + ); + + TransactionBuilder.signTransaction(tx, privateKey); + const txHash = (await client.sendRawTransaction(tx.serialize())).result; + await waitForTransactionReceipt(client, txHash); + }; + + burn = async ( + _amount: BigNumber | string, + privateKey: Crypto.PrivateKey, + payer: Crypto.Address + ) => { + const tx = TransactionBuilder.makeInvokeTransaction( + 'Burn', + [ + new Parameter('_amount', ParameterType.ByteArray, num2ByteArray(_amount)) + ], + contract.address, + '0', + '20000', + payer + ); + + TransactionBuilder.signTransaction(tx, privateKey); + const txHash = (await client.sendRawTransaction(tx.serialize())).result; + await waitForTransactionReceipt(client, txHash); + }; + + mint = async ( + _to: Crypto.Address, + _amount: BigNumber | string, + privateKey: Crypto.PrivateKey, + payer: Crypto.Address + ) => { + const tx = TransactionBuilder.makeInvokeTransaction( + 'Mint', + [ + new Parameter('_to', ParameterType.ByteArray, _to.serialize()), + new Parameter('_amount', ParameterType.ByteArray, num2ByteArray(_amount)) + ], + contract.address, + '0', + '20000', + payer + ); + + TransactionBuilder.signTransaction(tx, privateKey); + const txHash = (await client.sendRawTransaction(tx.serialize())).result; + await waitForTransactionReceipt(client, txHash); + }; + + // generate 10 accounts for test. + for (let i = 0; i < 10; ++i) { + const privateKey = Crypto.PrivateKey.random(); + const address = Crypto.Address.fromPubKey(privateKey.getPublicKey()); + randomAccount.push({ privateKey, address }); + } + }); + + it ('test', async () => { + + }); + + // it ('should not be able to be deployed by other.', async () => { + // const [ other ] = randomAccount; + // const tx = TransactionBuilder.makeInvokeTransaction('Deploy', [], contract.address, '0', '20000', other.address); + // TransactionBuilder.signTransaction(tx, other.privateKey); + // + // const txHash = (await client.sendRawTransaction(tx.serialize())).result; + // await waitForTransactionReceipt(client, txHash); + // + // // since the address is not deployer, this transaction must be rejected. + // (await isDeployed()).should.be.equal(false); + // }); + // + // it ('should deploy by deployer', async () => { + // const address = TestDeployer.forSign.address; + // const privateKey = TestDeployer.forSign.privateKey; + // const tx = TransactionBuilder.makeInvokeTransaction('Deploy', [], contract.address, '0', '20000', address); + // TransactionBuilder.signTransaction(tx, privateKey); + // + // const txHash = (await client.sendRawTransaction(tx.serialize())).result; + // await waitForTransactionReceipt(client, txHash); + // + // (await isDeployed()).should.be.equal(true); + // }); + // + // it ('should be owned by deployer', async () => { + // (await getOwner()).toBase58().should.be.equal(address.toBase58()); + // }); + // + // it ('should supply all tokens to the deployer', async () => { + // const supply = await totalSupply(); + // const deployerBalance = await getBalance(address); + // + // supply.toString().should.be.equal(deployerBalance.toString()); + // }); + // + // it ('should transfer tokens', async () => { + // const [ other ] = randomAccount; + // const transferValue = new BigNumber(1000); + // await transfer(address, other.address, transferValue, privateKey); + // + // (await getBalance(other.address)).toString().should.be.equal(transferValue.toString()); + // }); + // + // it ('should not transfer from other account', async () => { + // const [ _, other ] = randomAccount; + // await transfer(address, other.address, new BigNumber(1000), other.privateKey, other.address); + // + // const otherBalance = await getBalance(other.address); + // otherBalance.toString().should.be.equal(new BigNumber(0).toString()); + // }); + // + // it ('should not transfer over the owning', async () => { + // const [ other, another ] = randomAccount; + // const otherBalance = await getBalance(other.address); + // const anotherBalance = await getBalance(another.address); + // + // await transfer(other.address, another.address, otherBalance.plus(1000), other.privateKey); + // (await getBalance(another.address)).toString().should.be.equal(anotherBalance.toString()); + // }); + // + // it ('should not transfer minus amount', async () => { + // const [ _, other ] = randomAccount; + // + // // send to an address to 1000 tokens. + // await transfer(address, other.address, new BigNumber(1000), privateKey); + // const beforeBalance = await getBalance(other.address); + // + // // try to send to the address -1 token. + // await transfer(address, other.address, 'ff', privateKey); + // const afterBalance = await getBalance(other.address); + // + // // the first transfer transaction will be executed successfully, but the second + // // function call tries to send minus value, so it should not be executed. + // afterBalance.toString().should.be.equal(beforeBalance.toString()); + // }); + // + // it ('should burn the tokens', async () => { + // const balanceBeforeBurned = await getBalance(address); + // const supplyBeforeBurned = await totalSupply(); + // + // // burn 1000 tokens. + // await burn(new BigNumber(1000), privateKey, address); + // + // const balanceAfterBurned = await getBalance(address); + // const supplyAfterBurned = await totalSupply(); + // + // // since burning 1000 tokens, the balance after burning should be as small as 1000 + // // with the balance before burning. + // balanceAfterBurned.plus(1000).toString().should.be.equal(balanceBeforeBurned.toString()); + // + // // the total supply should also be changed. + // supplyAfterBurned.plus(1000).toString().should.be.equal(supplyBeforeBurned.toString()); + // }); + // + // it ('should not burn the tokens by other', async () => { + // const [ other ] = randomAccount; + // const supplyBeforeBurned = await totalSupply(); + // + // await burn(new BigNumber(1000), other.privateKey, other.address); + // + // const supplyAfterBurned = await totalSupply(); + // + // // the total supply should be equal since `burn` is called by other, not owner, so + // // the transaction should be rejected. + // supplyAfterBurned.toString().should.be.not.equal('0'); + // supplyBeforeBurned.toString().should.be.equal(supplyAfterBurned.toString()); + // }); + // + // it ('should not burn minus value', async () => { + // const balanceBeforeBurned = await getBalance(address); + // const supplyBeforeBurned = await totalSupply(); + // + // // try to burn -1 tokens. + // await burn('ff', privateKey, address); + // + // const balanceAfterBurned = await getBalance(address); + // const supplyAfterBurned = await totalSupply(); + // + // // should reject the `burn` call if minus value, so should minus value. + // balanceBeforeBurned.toString().should.be.equal(balanceAfterBurned.toString()); + // supplyBeforeBurned.toString().should.be.equal(supplyAfterBurned.toString()); + // }); + // + // it ('should not burn over the total supply', async () => { + // // calculate burning amount. The burning amount is [ total supply + 1000 ], + // // so it always satisfies that it is over the amount of balance the + // // owner has. + // const supplyBeforeBurned = await totalSupply(); + // const burnValue = supplyBeforeBurned.plus(1000); + // const supplyAfterBurned = await totalSupply(); + // + // // try to burn [ total supply + 1000 ] tokens. + // await burn(burnValue, privateKey, address); + // + // // the total supply should not be changed since trying to burn over the + // // total supply. (Or, if the owner balance is less than the burn amount, + // // it can be also rejected) + // supplyAfterBurned.toString().should.be.equal(supplyBeforeBurned.toString()); + // }); + // + // it ('should mint token', async () => { + // const mintValue = new BigNumber(1000); + // + // const supplyBeforeMinted = await totalSupply(); + // // try to mint 1000 tokens. + // await mint(address, mintValue, privateKey, address); + // const supplyAfterMinted = await totalSupply(); + // + // // the total supply should be changed. + // supplyBeforeMinted.plus(1000).toString().should.be.equal(supplyAfterMinted.toString()); + // }); + // + // it ('should not mint tokens by other', async () => { + // const [ other ] = randomAccount; + // const mintValue = new BigNumber(1000); + // + // const supplyBeforeMinted = await totalSupply(); + // + // // try to mint 1000 tokens by other address, not owner. + // await mint(other.address, mintValue, other.privateKey, other.address); + // const supplyAfterMinted = await totalSupply(); + // + // // the total supply should be equal since the `mint` call should be rejected. + // supplyAfterMinted.toString().should.be.equal(supplyBeforeMinted.toString()); + // }); + // + // it ('should not mint minus value', async () => { + // const mintValue = 'ff'; + // + // const supplyBeforeMinted = await totalSupply(); + // + // // try to mint -1 token. + // await mint(address, mintValue, privateKey, address); + // const supplyAfterMinted = await totalSupply(); + // + // // the total supply should not be changed since the `mint` call should be rejected. + // supplyAfterMinted.toString().should.be.equal(supplyBeforeMinted.toString()); + // }); + // + // it ('should approve token', async () => { + // const [ other ] = randomAccount; + // + // const approveValue = new BigNumber(1000); + // // const beforeApprove = await allowance(address, other.address); + // await approve(address, other.address, approveValue, privateKey, address); + // const allowed = await allowance(address, other.address); + // + // allowed.toString().should.be.equal(approveValue.toString()); + // }); + // + // it ('should not approve token by other', async () => { + // const [ _, other ] = randomAccount; + // + // const approveValue = new BigNumber(1000); + // await approve(address, other.address, approveValue, other.privateKey, other.address); + // const allowed = await allowance(address, other.address); + // + // allowed.toString().should.be.equal('0'); + // }); + // + // it ('should not approve over the own tokens', async () => { + // const [ other ] = randomAccount; + // const otherBalance = new BigNumber(await getBalance(other.address)); + // + // const beforeAllowed = await allowance(other.address, address); + // + // // try to approve balance + 1000, so try over the own + // const approveValue = otherBalance.plus(1000); + // await approve(other.address, address, approveValue, other.privateKey, other.address); + // + // const afterAllowed = await allowance(other.address, address); + // + // // allowance should not be changed since approving over the account balance should be rejected. + // afterAllowed.toString().should.be.equal(beforeAllowed.toString()); + // }); + // + // it ('should not approve minus value', async () => { + // const [ other ] = randomAccount; + // + // const beforeAllowed = await allowance(other.address, address); + // + // // try to approve -1 + // const approveValue = 'ff'; + // await approve(other.address, address, approveValue, other.privateKey, other.address); + // + // const afterAllowed = await allowance(other.address, address); + // + // // allowance should not be changed since approving minus value should be rejected. + // afterAllowed.toString().should.be.equal(beforeAllowed.toString()); + // }); + // + // it ('should transfer approved tokens', async () => { + // const [ other, another ] = randomAccount; + // const approveValue = new BigNumber(1000); + // const transferValue = new BigNumber(500); + // + // const beforeOwnerBalance = await getBalance(address); + // const beforeOtherBalance = await getBalance(other.address); + // const beforeAnotherBalance = await getBalance(another.address); + // + // // approve other address to use 1000 tokens by the owner address + // await approve(address, other.address, approveValue, privateKey, address); + // + // const allowed = await allowance(address, other.address); + // + // // allowance should be 1000 + // allowed.toString().should.be.equal(approveValue.toString()); + // + // // try to send 500 tokens from `owner` account to `another` account by `other` account + // await transferFrom(other.address, address, another.address, transferValue, other.privateKey, other.address); + // + // const afterOwnerBalance = await getBalance(address); + // const afterOtherBalance = await getBalance(other.address); + // const afterAnotherBalance = await getBalance(another.address); + // const afterAllowed = await allowance(address, other.address); + // + // // after transferred, allowance should be 500 + // afterAllowed.toString().should.be.equal(allowed.minus(500).toString()); + // + // // check changed balances + // afterOwnerBalance.plus(500).toString().should.be.equal(beforeOwnerBalance.toString()); + // afterOtherBalance.toString().should.be.equal(beforeOtherBalance.toString()); + // afterAnotherBalance.minus(500).toString().should.be.equal(beforeAnotherBalance.toString()); + // }); + // + // it ('should not transfer over the approved tokens', async () => { + // const [ other, another ] = randomAccount; + // const approveValue = new BigNumber(1000); + // const transferValue = new BigNumber(1500); + // + // const beforeOwnerBalance = await getBalance(address); + // const beforeOtherBalance = await getBalance(other.address); + // const beforeAnotherBalance = await getBalance(another.address); + // + // // approve other address to use 1000 tokens by the owner address + // await approve(address, other.address, approveValue, privateKey, address); + // + // const allowed = await allowance(address, other.address); + // + // // allowance should be 1000 + // allowed.toString().should.be.equal(approveValue.toString()); + // + // // try to send 1500 tokens from `owner` account to `another` account by `other` account + // await transferFrom(other.address, address, another.address, transferValue, other.privateKey, other.address); + // + // const afterOwnerBalance = await getBalance(address); + // const afterOtherBalance = await getBalance(other.address); + // const afterAnotherBalance = await getBalance(another.address); + // const afterAllowed = await allowance(address, other.address); + // + // // after transferred, allowance should be 500 + // afterAllowed.toString().should.be.equal(allowed.toString()); + // + // // check changed balances + // afterOwnerBalance.toString().should.be.equal(beforeOwnerBalance.toString()); + // afterOtherBalance.toString().should.be.equal(beforeOtherBalance.toString()); + // afterAnotherBalance.toString().should.be.equal(beforeAnotherBalance.toString()); + // }); + // + // it ('should not transfer from another address', async () => { + // const [ other, another ] = randomAccount; + // const approveValue = new BigNumber(1000); + // const transferValue = new BigNumber(500); + // + // const beforeOwnerBalance = await getBalance(address); + // const beforeOtherBalance = await getBalance(other.address); + // const beforeAnotherBalance = await getBalance(another.address); + // + // // approve other address to use 1000 tokens by the owner address + // await approve(address, other.address, approveValue, privateKey, address); + // + // const allowed = await allowance(address, other.address); + // + // // allowance should be 1000 + // allowed.toString().should.be.equal(approveValue.toString()); + // + // // try to send 500 tokens from `owner` account to `another` account by `another` account + // // it should be rejected + // await transferFrom(other.address, address, another.address, transferValue, another.privateKey, another.address); + // + // const afterOwnerBalance = await getBalance(address); + // const afterOtherBalance = await getBalance(other.address); + // const afterAnotherBalance = await getBalance(another.address); + // const afterAllowed = await allowance(address, other.address); + // + // // after transferred, allowance should be 500 + // afterAllowed.toString().should.be.equal(allowed.toString()); + // + // // check changed balances + // afterOwnerBalance.toString().should.be.equal(beforeOwnerBalance.toString()); + // afterOtherBalance.toString().should.be.equal(beforeOtherBalance.toString()); + // afterAnotherBalance.toString().should.be.equal(beforeAnotherBalance.toString()); + // }); + // + // it ('should not transfer ownership to other address by other', async () => { + // const [ other ] = randomAccount; + // const beforeOwner = await getOwner(); + // + // // try to transfer ownership by other address. + // // It should be rejected + // await transferOwnership(other.address, other.privateKey, other.address); + // const afterOwner = await getOwner(); + // + // beforeOwner.toBase58().should.be.equal(afterOwner.toBase58()); + // }); + // + // it ('should transfer ownership to other address by owner', async () => { + // const [ other ] = randomAccount; + // const beforeOwner = await getOwner(); + // + // // try to transfer ownership by owner. + // await transferOwnership(other.address, privateKey, address); + // const afterOwner = await getOwner(); + // + // // check owner changed + // beforeOwner.toBase58().should.be.not.equal(other.address.toBase58()); + // afterOwner.toBase58().should.be.equal(other.address.toBase58()); + // + // // new owner can mint and burn tokens + // const beforeSupply = await totalSupply(); + // + // await mint(other.address, new BigNumber(1000), other.privateKey, other.address); + // const afterMintSupply = await totalSupply(); + // + // // after mint, the total supply will be changed if success to transfer ownership + // beforeSupply.plus(1000).toString().should.be.equal(afterMintSupply.toString()); + // + // await burn(new BigNumber(1000), other.privateKey, other.address); + // const afterBurnSupply = await totalSupply(); + // + // // after burn, the total supply will be changed. + // afterBurnSupply.plus(1000).toString().should.be.equal(afterMintSupply.toString()); + // + // // restore the owner + // await transferOwnership(address, other.privateKey, other.address); + // const finalOwner = await getOwner(); + // + // finalOwner.toBase58().should.be.equal(beforeOwner.toBase58()); + // }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100755 index 0000000..bda12a0 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,63 @@ +{ + "include": [ + "index.ts", + "scripts/*.ts" + ], + "compilerOptions": { + /* Basic Options */ + "target": "es2016", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ + "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ + "lib": ["es2015", "es2016", "es2017"], + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + // "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + // "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + // "outDir": "./", /* Redirect output structure to the directory. */ + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + "strictNullChecks": false, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + + /* Module Resolution Options */ + // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + "types": ["node", "mocha"], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + } +} \ No newline at end of file diff --git a/utils/big-number.ts b/utils/big-number.ts new file mode 100755 index 0000000..1d52d12 --- /dev/null +++ b/utils/big-number.ts @@ -0,0 +1,20 @@ + +import { BigNumber } from 'bignumber.js'; +import { utils } from 'ontology-ts-sdk'; + +export function num2ByteArray(num: string | BigNumber) { + let hexAmount; + + if (typeof num !== 'string') { + hexAmount = num.toString(16); + if (hexAmount.length % 2 > 0) { + hexAmount = '0' + hexAmount; + } + + hexAmount = utils.reverseHex(hexAmount); + } else { + hexAmount = num; + } + + return hexAmount; +} \ No newline at end of file diff --git a/utils/cli.ts b/utils/cli.ts new file mode 100755 index 0000000..2d30abe --- /dev/null +++ b/utils/cli.ts @@ -0,0 +1,33 @@ + +import { join } from 'path'; +import { ConfigLoader } from './config-loader'; +import { Deployer } from './deployer'; +import * as glob from 'glob'; + +const chalk = require('chalk'); + +const commandLineArgs = require('command-line-args'); + +const OPTIONS = [ + { name: 'mode', alias: 'm', type: String }, + { name: 'deploy', type: Boolean } +]; + +async function main() { + const options = commandLineArgs(OPTIONS); + + const config = new ConfigLoader( + require(join(__dirname, '..', 'ontology.json')), + options.mode, + join(__dirname, '..') + ); + + if (options.deploy) { + const deployer = new Deployer(config); + const deployScripts = glob.sync(join(__dirname, '..', 'migrations', '*.ts')); + deployScripts.forEach((script) => require(script)(deployer)); + } +} + +main() + .catch(console.error); diff --git a/utils/config-loader.ts b/utils/config-loader.ts new file mode 100755 index 0000000..fc3847e --- /dev/null +++ b/utils/config-loader.ts @@ -0,0 +1,94 @@ + +import {Wallet, Account, Crypto, RestClient, RpcClient, WebsocketClient} from 'ontology-ts-sdk'; +import { join, parse } from 'path'; +import { readFileSync, readFile } from 'fs'; +import * as glob from 'glob'; + +import { OntologyConfig } from './models'; + +export class ConfigLoader { + avm: { [contract: string]: string } = {}; + client: RestClient | RpcClient | WebsocketClient; + forSign: { + address: Crypto.Address, + privateKey: Crypto.PrivateKey; + } = { address: undefined, privateKey: undefined }; + + constructor( + private options: OntologyConfig, + private mode: string, + private workingDirectory: string + ) { + this.loadAvm(); + this.loadClient(); + this.loadAccount(); + } + + private loadAvm(): void { + const matchFiles = glob.sync( + join(this.workingDirectory, this.options.avmFiles), { ignore: '**/node_modules/**', absolute: true } + ); + + matchFiles.forEach((filePath: string) => { + const basename = parse(filePath).name; + Object.assign(this.avm, { [basename]: readFileSync(filePath).toString('hex') }); + }); + } + + private loadClient(): void { + const method = this.option.method; + const url = this.option.host; + + switch (method) { + case 'rpc': + this.client = new RpcClient(url); + break; + case 'rest': + this.client = new RestClient(url); + break; + case 'websocket': + this.client = new WebsocketClient(url); + break; + } + } + + private loadAccount(): void { + const wallet = this.option.wallet; + + if (wallet.privateKey !== undefined) { + this.forSign.address = new Crypto.Address(wallet.address); + this.forSign.privateKey = new Crypto.PrivateKey(wallet.privateKey); + } else { + const walletInfo = Wallet.fromWalletFile(JSON.parse(readFileSync(join(this.workingDirectory, wallet.file)).toString())); + + // select account. If default account is set, search the address and select it + // or select the first account if not exist. + let account = undefined; + if (walletInfo.defaultAccountAddress) { + account = walletInfo.accounts.filter( + (account: Account) => account.address.value === walletInfo.defaultAccountAddress + )[0]; + } + + if (account === undefined) { + account = walletInfo.accounts[0]; + } + + // get the address and private key from default account + this.forSign.address = account.address; + this.forSign.privateKey = account.encryptedKey.decrypt(wallet.password, account.address, account.salt, { + cost: walletInfo.scrypt.n, + blockSize: walletInfo.scrypt.r, + parallel: walletInfo.scrypt.p, + size: walletInfo.scrypt.dkLen + }); + } + } + + get option() { + return this.options.network[this.mode]; + } +} + +const defaultConfig = require(join(__dirname, '..', 'ontology.json')); +export const TestConfig = new ConfigLoader(defaultConfig, defaultConfig.testNetwork, join(__dirname, '..')); diff --git a/utils/deployer.ts b/utils/deployer.ts new file mode 100755 index 0000000..4ef03e5 --- /dev/null +++ b/utils/deployer.ts @@ -0,0 +1,91 @@ +import { ConfigLoader } from "./config-loader"; +import {TransactionBuilder, RestClient, RpcClient, WebsocketClient, Crypto, Transaction, utils} from 'ontology-ts-sdk'; +import { waitForTransactionReceipt } from './transaction'; + +export class Deployer { + forSign: { + address: Crypto.Address, + privateKey: Crypto.PrivateKey + } = null; + + constructor( + private config: ConfigLoader + ) { + this.forSign = config.forSign; + } + + async deploy(contractName: string, initFunc?: string) { + const avm = this.config.avm[contractName]; + const privateKey = this.config.forSign.privateKey; + const payer = this.config.forSign.address; + const client = this.config.client; + + const tx = TransactionBuilder.makeDeployCodeTransaction( + avm, + '', + '', + '', + '', + '', + true, + '5', + '20400000', + payer + ); + + TransactionBuilder.signTransaction(tx, privateKey); + await this.client.sendRawTransaction(tx.serialize()); + + return new DeployedTransaction(client, avm, tx, initFunc, payer, privateKey); + } + + get client() { + return this.config.client; + } +} + +export class DeployedTransaction { + public address: Crypto.Address; + public codeHash: string; + + constructor( + private client: RestClient | RpcClient | WebsocketClient, + private avm: string, + private tx: Transaction, + private initFunc?: string, + private payer?: Crypto.Address, + private privateKey?: Crypto.PrivateKey + ) { + this.codeHash = Crypto.Address.fromVmCode(this.avm).toHexString(); + this.address = new Crypto.Address(utils.reverseHex(this.codeHash)); + } + + async deployed() { + // send getContractJson for every 1 second and check + // the contract code is deployed. + // if not deployed over 60 seconds, return error. + const codeDeployTxReceipt = await this.waitForTransactionReceipt(utils.reverseHex(this.tx.getSignContent())); + + // if initFunc is defined, call and wait the function call transaction. + if (this.initFunc) { + const tx = TransactionBuilder.makeInvokeTransaction( + this.initFunc, + [], + this.address, + '500', + '20000', + this.payer + ); + TransactionBuilder.signTransaction(tx, this.privateKey); + await this.client.sendRawTransaction(tx.serialize()); + return await this.waitForTransactionReceipt(utils.reverseHex(tx.getSignContent())); + } + return codeDeployTxReceipt; + } + + private async waitForTransactionReceipt(txHash: string, options?: any) { + return await waitForTransactionReceipt(this.client, txHash, options); + } +} + +export const TestDeployer = new Deployer(require('./config-loader').TestConfig); diff --git a/utils/index.ts b/utils/index.ts new file mode 100755 index 0000000..e7f9820 --- /dev/null +++ b/utils/index.ts @@ -0,0 +1,3 @@ +export * from './models'; +export * from './cli'; +export * from './config-loader'; diff --git a/utils/models/config.ts b/utils/models/config.ts new file mode 100755 index 0000000..bf32b10 --- /dev/null +++ b/utils/models/config.ts @@ -0,0 +1,11 @@ + +export interface OntologyConfig { + avmFiles: string; + network: { + [networkType: string]: { + method: 'rpc' | 'rest' | 'websocket'; + host: string; + wallet: any; + } + }; +} \ No newline at end of file diff --git a/utils/models/index.ts b/utils/models/index.ts new file mode 100755 index 0000000..f03c228 --- /dev/null +++ b/utils/models/index.ts @@ -0,0 +1 @@ +export * from './config'; diff --git a/utils/transaction.ts b/utils/transaction.ts new file mode 100755 index 0000000..2013385 --- /dev/null +++ b/utils/transaction.ts @@ -0,0 +1,39 @@ + +/** + * Waits until mining a transaction. + * @param client client + * @param txHash transaction hash to wait. + * @param options options for transaction wait. + */ +export async function waitForTransactionReceipt(client: any, txHash: string, options?: any): Promise { + + if (!options) { + options = {}; + } + + const checkInterval = options.checkInterval || 1000; + const timeout = options.timeout || 60000; + const checkOut = Math.ceil(timeout / checkInterval); + + return await new Promise((resolve, reject) => { + let checkCnt = 0; + const minedCheckHandler = setInterval(() => { + client.getRawTransactionJson(txHash).then((result: any) => { + if (result.error === 0) { + clearTimeout(minedCheckHandler); + resolve(result); + } + + else { + if (checkCnt > checkOut) { + clearTimeout(minedCheckHandler); + reject(new Error(`failed to mined over ${timeout} seconds`)); + } else { + ++checkCnt; + } + } + + }); + }, checkInterval); + }); +} diff --git a/wallet/wallet_local.dat b/wallet/wallet_local.dat new file mode 100644 index 0000000..3eb1683 --- /dev/null +++ b/wallet/wallet_local.dat @@ -0,0 +1,44 @@ +{ + "name": "MyWallet", + "version": "1.1", + "createTime":"", + "defaultAccountAddress":"", + "defaultOntid":"", + "identities": [ + ], + "scrypt": { + "p": 8, + "n": 16384, + "r": 8, + "dkLen": 64 + }, + "accounts": [{ + "address": "Ac725LuR7wo481zvNmc9jerqCzoCArQjtw", + "enc-alg": "aes-256-gcm", + "key": "maV8N0by6v5rXkHLABrAlYt/RrQDyp5d+oYXHC4Qjky4iPdCJ9qxt2bub4PtbZ6Q", + "algorithm": "ECDSA", + "salt": "ccI8UmFASdiUcTAWGEcl4A==", + "parameters": { + "curve": "P-256" + }, + "label": "", + "publicKey": "02694a1621123269afc7774ac5e72eae1362ca81721834992cb470a6699337e1cb", + "signatureScheme": "SHA256withECDSA", + "isDefault": true, + "lock": false + }, { + "address": "AcSfDw3dTAU2TP1QFVoxc1F7WQDypmzFqM", + "enc-alg": "aes-256-gcm", + "key": "xgBO3/i2EquGZCvQDJxnJsndAqaGHtMAuUgITiO7r8X+0lSoR7/CvDs/RePrRCwQ", + "algorithm": "ECDSA", + "salt": "X3RUfM1Qeh/IOWb1oO/wWQ==", + "parameters": { + "curve": "P-256" + }, + "label": "", + "publicKey": "02feaf11952127cbf38792d6a46e46e41e1ef50b7f4d79273c93a5bd5ee131aa59", + "signatureScheme": "SHA256withECDSA", + "isDefault": false, + "lock": false + }] +}