craft-nft

A standalone NFT implementation for real-world arts and crafts assets
Log | Files | Refs | README

commit 685314c0fc0929b162890bb8927337413905e269
parent 0bc71c3815e9626428f5dbab45079f5d25429680
Author: lash <dev@holbrook.no>
Date:   Mon, 19 Dec 2022 09:31:09 +0000

Add makefiles, python packaging

Diffstat:
M.gitignore | 7++++++-
AMakefile | 15+++++++++++++++
Ajs/Makefile | 3+++
Mjs/manual_test_browser.js | 1+
Mjs/src/engine.js | 6------
Apython/MANIFEST.in | 1+
Apython/Makefile | 2++
Apython/craft_nft/__init__.py | 2++
Rpython/eth_craft_nft/data/CraftNFT.bin -> python/craft_nft/data/CraftNFT.bin | 0
Rpython/eth_craft_nft/data/CraftNFT.json -> python/craft_nft/data/CraftNFT.json | 0
Rpython/eth_craft_nft/error.py -> python/craft_nft/error.py | 0
Apython/craft_nft/eth.py | 8++++++++
Apython/craft_nft/nft.py | 276+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apython/craft_nft/runnable/dump.py | 143+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apython/craft_nft/runnable/publish.py | 127+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dpython/eth_craft_nft/__init__.py | 1-
Dpython/eth_craft_nft/nft.py | 276-------------------------------------------------------------------------------
Dpython/eth_craft_nft/runnable/dump.py | 141-------------------------------------------------------------------------------
Dpython/eth_craft_nft/runnable/publish.py | 127-------------------------------------------------------------------------------
Mpython/requirements.txt | 1+
Apython/setup.cfg | 37+++++++++++++++++++++++++++++++++++++
Apython/setup.py | 26++++++++++++++++++++++++++
Mpython/tests/test_basic.py | 6+++---
Mpython/tests/test_numbered.py | 4++--
Mpython/tests/test_spec.py | 5++---
Mpython/tests/test_supply.py | 4++--
Msolidity/Makefile | 2+-
27 files changed, 658 insertions(+), 563 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -1,5 +1,10 @@ __pycache__ *.pyc -egg-info/* +*.egg-info* solidity/*.bin solidity/*.json +python/dist +js/dist +js/node_modules +package-lock.json +js/settings.json diff --git a/Makefile b/Makefile @@ -0,0 +1,15 @@ +all: solidity js python + +.PHONY: js python solidity + +js: + make -C js + +python: + make -C python + +solidity: + make -C solidity install + +test: + make -C python test diff --git a/js/Makefile b/js/Makefile @@ -0,0 +1,3 @@ +all: + npm install + npm run build diff --git a/js/manual_test_browser.js b/js/manual_test_browser.js @@ -173,6 +173,7 @@ async function uiCreateToken() { async function run(w3, generated_session) { session = generated_session; + console.debug('running with session', session); session.contentGateway = new Wala('http://localhost:8001'); const account = document.getElementById('data_account'); let s = document.createElement('span'); diff --git a/js/src/engine.js b/js/src/engine.js @@ -26,7 +26,6 @@ async function startSession(w3, config, session, runner) { session.name = await session.contract.methods.name().call({from: session.account}); session.symbol = await session.contract.methods.symbol().call({from: session.account}); session.supply = await session.contract.methods.totalSupply().call({from: session.account}); - console.debug('session', session); runner(w3, session); } @@ -45,7 +44,6 @@ async function getTokens(w3, session, callback) { } catch(e) { break; }; - console.debug('found token', token); i++; } } @@ -60,7 +58,6 @@ async function allocateToken(session, tokenId, amount) { async function mintToken(session, tokenId, batch, recipient) { const w3 = new Web3(); const address = await w3.utils.toChecksumAddress(recipient); - console.log('address', address, recipient); session.contract.methods.mintFromBatchTo(address, '0x' + tokenId, batch).send({ from: session.account, value: 0, @@ -116,7 +113,6 @@ async function isMintAvailable(session, tokenId, batch) { } async function toToken(session, tokenId, tokenContent) { - console.log('process token content', tokenContent); if (tokenId.substring(0, 2) == '0x') { tokenId = tokenId.substring(2); } @@ -126,7 +122,6 @@ async function toToken(session, tokenId, tokenContent) { } const v = parseInt(tokenContent.substring(0, 2), 16); - console.debug('vvv', v); let data = { tokenId: tokenId, minted: false, @@ -180,7 +175,6 @@ async function toToken(session, tokenId, tokenContent) { } async function getTokenChainData(session, tokenId) { - console.log('query for', tokenId); const v = await session.contract.methods.mintedToken('0x' + tokenId).call({from: session.account}); const mintedToken = await toToken(session, tokenId, v); diff --git a/python/MANIFEST.in b/python/MANIFEST.in @@ -0,0 +1 @@ +include craft_nft/data/* *requirements.txt diff --git a/python/Makefile b/python/Makefile @@ -0,0 +1,2 @@ +all: + python setup.py sdist diff --git a/python/craft_nft/__init__.py b/python/craft_nft/__init__.py @@ -0,0 +1,2 @@ +# local imports +from .nft import CraftNFT diff --git a/python/eth_craft_nft/data/CraftNFT.bin b/python/craft_nft/data/CraftNFT.bin diff --git a/python/eth_craft_nft/data/CraftNFT.json b/python/craft_nft/data/CraftNFT.json diff --git a/python/eth_craft_nft/error.py b/python/craft_nft/error.py diff --git a/python/craft_nft/eth.py b/python/craft_nft/eth.py @@ -0,0 +1,8 @@ +# standard imports +import aenum + +# external imports +from chainlib.eth.contract import ABIContractType + +aenum.extend_enum(ABIContractType, 'UINT48', 'uint48') + diff --git a/python/craft_nft/nft.py b/python/craft_nft/nft.py @@ -0,0 +1,276 @@ +# standard imports +import os +import logging + +# external imports +from chainlib.eth.tx import TxFormat +from eth_erc721 import ERC721 +from hexathon import add_0x +from hexathon import strip_0x +from chainlib.eth.contract import ABIContractEncoder +from chainlib.eth.contract import ABIContractDecoder +from chainlib.eth.contract import abi_decode_single +from chainlib.jsonrpc import JSONRPCRequest +from chainlib.eth.constant import ZERO_ADDRESS +from chainlib.eth.constant import ZERO_CONTENT +from chainlib.eth.address import to_checksum_address + +# local imports +from .error import InvalidBatchError +from .eth import ABIContractType + +moddir = os.path.dirname(__file__) +datadir = os.path.join(moddir, 'data') + +INVALID_BATCH = (2**256)-1 + +logg = logging.getLogger(__name__) + +class TokenSpec: + + def __init__(self, count, cursor): + self.count = count + self.cursor = cursor + + + def __str__(self): + return '{} / {}'.format(self.cursor, self.count) + + +class MintedToken: + + def __init__(self, owner_address=ZERO_ADDRESS, token_id=None, batched=False, minted=False): + self.minted = minted + self.batched = batched + self.owner = owner_address + self.index = 0 + self.batch = 0 + self.token_id = token_id + + + def __str__(self): + owner = to_checksum_address(self.owner) + if self.batched: + return '{} owned {}'.format( + self.token_id, + owner, + ) + return '{} batch {} index {} owned by {}'.format( + self.token_id, + self.batch, + self.index, + owner, + ) + + +class CraftNFT(ERC721): + + __abi = None + __bytecode = None + + @staticmethod + def abi(): + if CraftNFT.__abi == None: + f = open(os.path.join(datadir, 'CraftNFT.json'), 'r') + CraftNFT.__abi = json.load(f) + f.close() + return CraftNFT.__abi + + + @staticmethod + def bytecode(): + if CraftNFT.__bytecode == None: + f = open(os.path.join(datadir, 'CraftNFT.bin')) + CraftNFT.__bytecode = f.read() + f.close() + return CraftNFT.__bytecode + + + @staticmethod + def gas(code=None): + return 4000000 + + + def constructor(self, sender_address, name, symbol, tx_format=TxFormat.JSONRPC): + code = CraftNFT.bytecode() + enc = ABIContractEncoder() + enc.string(name) + enc.string(symbol) + code += enc.get() + tx = self.template(sender_address, None, use_nonce=True) + tx = self.set_code(tx, code) + return self.finalize(tx, tx_format) + + + def allocate(self, contract_address, sender_address, token_id, amount=0, tx_format=TxFormat.JSONRPC): + enc = ABIContractEncoder() + enc.method('allocate') + enc.typ(ABIContractType.BYTES32) + enc.typ(ABIContractType.UINT48) + enc.bytes32(token_id) + enc.uintn(amount, 48) + data = enc.get() + tx = self.template(sender_address, contract_address, use_nonce=True) + tx = self.set_code(tx, data) + tx = self.finalize(tx, tx_format) + return tx + + + def token_at(self, contract_address, idx, sender_address=ZERO_ADDRESS, id_generator=None): + j = JSONRPCRequest(id_generator) + o = j.template() + o['method'] = 'eth_call' + enc = ABIContractEncoder() + enc.method('tokens') + enc.typ(ABIContractType.UINT256) + enc.uint256(idx) + data = add_0x(enc.get()) + tx = self.template(sender_address, contract_address) + tx = self.set_code(tx, data) + o['params'].append(self.normalize(tx)) + o['params'].append('latest') + o = j.finalize(o) + return o + + + def batch_of(self, conn, contract_address, token_id, super_index, sender_address=ZERO_ADDRESS, id_generator=None): + i = 0 + c = 0 + + while True: + o = self.get_token_spec(contract_address, token_id, i, sender_address=sender_address) + try: + r = conn.do(o) + except: + break + spec = self.parse_token_spec(r) + c += spec.count + if super_index < c: + return i + i += 1 + + raise ValueError(super_index) + + + def get_token_spec(self, contract_address, token_id, batch, sender_address=ZERO_ADDRESS, id_generator=None): + j = JSONRPCRequest(id_generator) + o = j.template() + o['method'] = 'eth_call' + enc = ABIContractEncoder() + enc.method('token') + enc.typ(ABIContractType.BYTES32) + enc.typ(ABIContractType.UINT256) + enc.bytes32(token_id) + enc.uint256(batch) + data = add_0x(enc.get()) + tx = self.template(sender_address, contract_address) + tx = self.set_code(tx, data) + o['params'].append(self.normalize(tx)) + o['params'].append('latest') + o = j.finalize(o) + return o + + + def get_token(self, contract_address, token_id, sender_address=ZERO_ADDRESS, id_generator=None): + j = JSONRPCRequest(id_generator) + o = j.template() + o['method'] = 'eth_call' + enc = ABIContractEncoder() + enc.method('mintedToken') + enc.typ(ABIContractType.BYTES32) + enc.bytes32(token_id) + data = add_0x(enc.get()) + tx = self.template(sender_address, contract_address) + tx = self.set_code(tx, data) + o['params'].append(self.normalize(tx)) + o['params'].append('latest') + o = j.finalize(o) + return o + + + def get_digest(self, contract_address, token_id, sender_address=ZERO_ADDRESS, id_generator=None): + j = JSONRPCRequest(id_generator) + o = j.template() + o['method'] = 'eth_call' + enc = ABIContractEncoder() + enc.method('getDigest') + enc.typ(ABIContractType.BYTES32) + enc.bytes32(token_id) + data = add_0x(enc.get()) + tx = self.template(sender_address, contract_address) + tx = self.set_code(tx, data) + o['params'].append(self.normalize(tx)) + o['params'].append('latest') + o = j.finalize(o) + return o + + + def mint_to(self, contract_address, sender_address, recipient, token_id, batch, index=None, tx_format=TxFormat.JSONRPC): + enc = ABIContractEncoder() + + if index != None: + enc.method('mintExactFromBatchTo') + enc.typ(ABIContractType.ADDRESS) + enc.typ(ABIContractType.BYTES32) + enc.typ(ABIContractType.UINT16) + enc.typ(ABIContractType.UINT48) + enc.address(recipient) + enc.bytes32(token_id) + enc.uintn(batch, 16) + enc.uintn(index, 48) + data = enc.get() + else: + enc.method('mintFromBatchTo') + enc.typ(ABIContractType.ADDRESS) + enc.typ(ABIContractType.BYTES32) + enc.typ(ABIContractType.UINT16) + enc.address(recipient) + enc.bytes32(token_id) + enc.uintn(batch, 16) + data = enc.get() + + tx = self.template(sender_address, contract_address, use_nonce=True) + tx = self.set_code(tx, data) + tx = self.finalize(tx, tx_format) + return tx + + + # def to_index_id(self, token_id, batch, index): + + + @classmethod + def parse_batch_of(self, v): + r = abi_decode_single(ABIContractType.UINT256, v) + if r == INVALID_BATCH: + raise InvalidBatchError() + return r + + + @classmethod + def parse_token_spec(self, v): + v = strip_0x(v) + d = ABIContractDecoder() + d.typ(ABIContractType.UINT48) + d.typ(ABIContractType.UINT48) + d.val(v[:64]) + d.val(v[64:128]) + r = d.decode() + return TokenSpec(r[0], r[1]) + + @classmethod + def parse_token(self, v, token_id): + v = strip_0x(v) + if v == strip_0x(ZERO_CONTENT): + return MintedToken() + + token_id = strip_0x(token_id) + c = v[:2] + addr = v[24:] + if int(c, 16) & 0x40 > 0: + return MintedToken(addr, token_id=token_id, batched=True, minted=True) + + o = MintedToken(addr, minted=True) + o.batch = int(token_id[48:52], 16) + o.index = int(token_id[52:64], 16) + o.token_id = token_id[:48] + v[2:18] + return o diff --git a/python/craft_nft/runnable/dump.py b/python/craft_nft/runnable/dump.py @@ -0,0 +1,143 @@ +"""Deploys badge NFT + +.. moduleauthor:: Louis Holbrook <dev@holbrook.no> +.. pgp:: 0826EDA1702D1E87C6E2875121D2E7BB88C2A746 + +""" + +# SPDX-License-Identifier: GPL-3.0-or-later + +# standard imports +import sys +import os +import json +import argparse +import logging +import time +from enum import Enum + +# external imports +import chainlib.eth.cli +from chainlib.chain import ChainSpec +from chainlib.eth.constant import ZERO_ADDRESS +from chainlib.settings import ChainSettings +from chainlib.eth.settings import process_settings +from chainlib.eth.cli.arg import Arg +from chainlib.eth.cli.arg import ArgFlag +from chainlib.eth.cli.arg import process_args +from chainlib.eth.cli.log import process_log +from chainlib.eth.cli.config import Config +from chainlib.eth.cli.config import process_config +from hexathon import strip_0x +from hexathon import add_0x + +# local imports +from craft_nft import CraftNFT + +logg = logging.getLogger() + +def process_config_local(config, arg, args, flags): + contract = None + try: + contract = config.get('_EXEC_ADDRESS') + except KeyError: + pass + + if contract == None: + address = config.get('_POSARG') + if address: + contract = add_0x(address) + else: + contract = stdin_arg() + + config.add(contract, '_CONTRACT', False) + + config.add(100000, '_FEE_LIMIT', True) + return config + + +arg_flags = ArgFlag() +arg = Arg(arg_flags) + +flags = arg_flags.STD_READ | arg_flags.EXEC | arg_flags.TAB + +argparser = chainlib.eth.cli.ArgumentParser() +argparser = process_args(argparser, arg, flags) +argparser.add_argument('contract_address', type=str, help='Token contract address (may also be specified by -e)') +args = argparser.parse_args() + +logg = process_log(args, logg) + +config = Config() +config = process_config(config, arg, args, flags, positional_name='contract_address') +config = process_config_local(config, arg, args, flags) +logg.debug('config loaded:\n{}'.format(config)) + +settings = ChainSettings() +settings = process_settings(settings, config) +logg.debug('settings loaded:\n{}'.format(settings)) + + +def render_token_batches(c, conn, token_address, token_id, w=sys.stdout): + i = 0 + while True: + o = c.get_token_spec(token_address, token_id, i) + r = None + try: + r = conn.do(o) + except: + break + spec = c.parse_token_spec(r) + + for j in range(spec.cursor): + cursor_hex = j.to_bytes(6, byteorder='big').hex() + batch_hex = i.to_bytes(2, byteorder='big').hex() + token_id_indexed = token_id[:48] + batch_hex + cursor_hex + render_token_mint(c, conn, token_address, token_id_indexed, w=w) + + i += 1 + + +def render_token_mint(c, conn, token_address, token_id, w=sys.stdout): + o = c.get_token(token_address, token_id) + r = conn.do(o) + token = c.parse_token(r, token_id) + if token.minted: + w.write('token {}\n'.format(token)) + + +def render_token(c, conn, token_address, token_id, w=sys.stdout): + token_id = strip_0x(token_id) + o = c.get_token_spec(token_address, token_id, 0) + r = conn.do(o) + spec = c.parse_token_spec(r) + if spec.count > 0: + return render_token_batches(c, conn, token_address, token_id, w=sys.stdout) + + return render_token_mint(c, conn, token_address, token_id, w=w) + + +def main(): + token_address = config.get('_CONTRACT') + conn = settings.get('CONN') + c = CraftNFT( + chain_spec=settings.get('CHAIN_SPEC'), + gas_oracle=settings.get('GAS_ORACLE'), + ) + + outkeys = config.get('_OUTARG') + + i = 0 + while True: + o = c.token_at(token_address, i) + r = None + try: + r = conn.do(o) + except: + break + render_token(c, conn, token_address, r) + i += 1 + + +if __name__ == '__main__': + main() diff --git a/python/craft_nft/runnable/publish.py b/python/craft_nft/runnable/publish.py @@ -0,0 +1,127 @@ +"""Deploys badge NFT + +.. moduleauthor:: Louis Holbrook <dev@holbrook.no> +.. pgp:: 0826EDA1702D1E87C6E2875121D2E7BB88C2A746 + +""" + +# SPDX-License-Identifier: GPL-3.0-or-later + +# standard imports +import sys +import os +import json +import argparse +import logging +import time +from enum import Enum + +# external imports +import chainlib.eth.cli +from chainlib.chain import ChainSpec +from chainlib.eth.constant import ZERO_ADDRESS +from chainlib.settings import ChainSettings +from chainlib.eth.settings import process_settings +from chainlib.eth.cli.arg import Arg +from chainlib.eth.cli.arg import ArgFlag +from chainlib.eth.cli.arg import process_args +from chainlib.eth.cli.log import process_log +from chainlib.eth.cli.config import Config +from chainlib.eth.cli.config import process_config + +# local imports +from craft_nft import CraftNFT + +logg = logging.getLogger() + + +def process_config_local(config, arg, args, flags): + config.add(args.name, '_TOKEN_NAME', False) + config.add(args.symbol, '_TOKEN_SYMBOL', False) + return config + + +arg_flags = ArgFlag() +arg = Arg(arg_flags) +flags = arg_flags.STD_WRITE | arg_flags.WALLET | arg_flags.CREATE | arg_flags.VALUE | arg_flags.TAB + +argparser = chainlib.eth.cli.ArgumentParser() +argparser.add_argument('--name', type=str, required=True, help='Token name') +argparser.add_argument('--symbol', type=str, required=True, help='Token symbol') +argparser = process_args(argparser, arg, flags) +args = argparser.parse_args(sys.argv[1:]) + +logg = process_log(args, logg) + +config = Config() +config = process_config(config, arg, args, flags) +config = process_config_local(config, arg, args, flags) +logg.debug('config loaded:\n{}'.format(config)) + +settings = ChainSettings() +settings = process_settings(settings, config) +logg.debug('settings loaded:\n{}'.format(settings)) + + +#arg_flags = chainlib.eth.cli.argflag_std_write +#argparser = chainlib.eth.cli.ArgumentParser(arg_flags) +#argparser.add_argument('--name', dest='token_name', type=str, help='Token name') +#argparser.add_argument('--symbol', dest='token_symbol', type=str, help='Token symbol') +#args = argparser.parse_args() +# +#extra_args = { +# 'token_name': None, +# 'token_symbol': None, +# } +#config = chainlib.eth.cli.Config.from_args(args, arg_flags, extra_args=extra_args, default_fee_limit=CraftNFT.gas()) + +#wallet = chainlib.eth.cli.Wallet() +#wallet.from_config(config) + +#rpc = chainlib.eth.cli.Rpc(wallet=wallet) +#conn = rpc.connect_by_config(config) + +#chain_spec = ChainSpec.from_chain_str(config.get('CHAIN_SPEC')) + + +def main(): + #signer = rpc.get_signer() + #signer_address = rpc.get_sender_address() + + token_name = config.get('_TOKEN_NAME') + token_symbol = config.get('_TOKEN_SYMBOL') + conn = settings.get('CONN') + +# gas_oracle = rpc.get_gas_oracle() +# nonce_oracle = rpc.get_nonce_oracle() + + c = CraftNFT( + settings.get('CHAIN_SPEC'), + signer=settings.get('SIGNER'), + gas_oracle=settings.get('FEE_ORACLE'), + nonce_oracle=settings.get('NONCE_ORACLE') + ) + + (tx_hash_hex, o) = c.constructor( + settings.get('SENDER_ADDRESS'), + token_name, + token_symbol, + ) + if config.get('_RPC_SEND'): + conn.do(o) + if config.true('_WAIT'): + r = conn.wait(tx_hash_hex) + if r['status'] == 0: + sys.stderr.write('EVM revert while deploying contract. Wish I had more to tell you') + sys.exit(1) + # TODO: pass through translator for keys (evm tester uses underscore instead of camelcase) + address = r['contractAddress'] + + print(address) + else: + print(tx_hash_hex) + else: + print(o) + +if __name__ == '__main__': + main() diff --git a/python/eth_craft_nft/__init__.py b/python/eth_craft_nft/__init__.py @@ -1 +0,0 @@ -from .nft import CraftNFT diff --git a/python/eth_craft_nft/nft.py b/python/eth_craft_nft/nft.py @@ -1,276 +0,0 @@ -# standard imports -import os -import logging - -# external imports -from chainlib.eth.tx import TxFormat -from eth_erc721 import ERC721 -from hexathon import add_0x -from hexathon import strip_0x -from chainlib.eth.contract import ABIContractEncoder -from chainlib.eth.contract import ABIContractDecoder -from chainlib.eth.contract import ABIContractType -from chainlib.eth.contract import abi_decode_single -from chainlib.jsonrpc import JSONRPCRequest -from chainlib.eth.constant import ZERO_ADDRESS -from chainlib.eth.constant import ZERO_CONTENT -from chainlib.eth.address import to_checksum_address - -# local imports -from eth_craft_nft.error import InvalidBatchError - -moddir = os.path.dirname(__file__) -datadir = os.path.join(moddir, 'data') - -INVALID_BATCH = (2**256)-1 - -logg = logging.getLogger(__name__) - -class TokenSpec: - - def __init__(self, count, cursor): - self.count = count - self.cursor = cursor - - - def __str__(self): - return '{} / {}'.format(self.cursor, self.count) - - -class MintedToken: - - def __init__(self, owner_address=ZERO_ADDRESS, token_id=None, batched=False, minted=False): - self.minted = minted - self.batched = batched - self.owner = owner_address - self.index = 0 - self.batch = 0 - self.token_id = token_id - - - def __str__(self): - owner = to_checksum_address(self.owner) - if self.batched: - return '{} owned {}'.format( - self.token_id, - owner, - ) - return '{} batch {} index {} owned by {}'.format( - self.token_id, - self.batch, - self.index, - owner, - ) - - -class CraftNFT(ERC721): - - __abi = None - __bytecode = None - - @staticmethod - def abi(): - if CraftNFT.__abi == None: - f = open(os.path.join(datadir, 'CraftNFT.json'), 'r') - CraftNFT.__abi = json.load(f) - f.close() - return CraftNFT.__abi - - - @staticmethod - def bytecode(): - if CraftNFT.__bytecode == None: - f = open(os.path.join(datadir, 'CraftNFT.bin')) - CraftNFT.__bytecode = f.read() - f.close() - return CraftNFT.__bytecode - - - @staticmethod - def gas(code=None): - return 4000000 - - - def constructor(self, sender_address, name, symbol, tx_format=TxFormat.JSONRPC): - code = CraftNFT.bytecode() - enc = ABIContractEncoder() - enc.string(name) - enc.string(symbol) - code += enc.get() - tx = self.template(sender_address, None, use_nonce=True) - tx = self.set_code(tx, code) - return self.finalize(tx, tx_format) - - - def allocate(self, contract_address, sender_address, token_id, amount=0, tx_format=TxFormat.JSONRPC): - enc = ABIContractEncoder() - enc.method('allocate') - enc.typ(ABIContractType.BYTES32) - enc.typ(ABIContractType.UINT48) - enc.bytes32(token_id) - enc.uintn(amount, 48) - data = enc.get() - tx = self.template(sender_address, contract_address, use_nonce=True) - tx = self.set_code(tx, data) - tx = self.finalize(tx, tx_format) - return tx - - - def token_at(self, contract_address, idx, sender_address=ZERO_ADDRESS, id_generator=None): - j = JSONRPCRequest(id_generator) - o = j.template() - o['method'] = 'eth_call' - enc = ABIContractEncoder() - enc.method('tokens') - enc.typ(ABIContractType.UINT256) - enc.uint256(idx) - data = add_0x(enc.get()) - tx = self.template(sender_address, contract_address) - tx = self.set_code(tx, data) - o['params'].append(self.normalize(tx)) - o['params'].append('latest') - o = j.finalize(o) - return o - - - def batch_of(self, conn, contract_address, token_id, super_index, sender_address=ZERO_ADDRESS, id_generator=None): - i = 0 - c = 0 - - while True: - o = self.get_token_spec(contract_address, token_id, i, sender_address=sender_address) - try: - r = conn.do(o) - except: - break - spec = self.parse_token_spec(r) - c += spec.count - if super_index < c: - return i - i += 1 - - raise ValueError(super_index) - - - def get_token_spec(self, contract_address, token_id, batch, sender_address=ZERO_ADDRESS, id_generator=None): - j = JSONRPCRequest(id_generator) - o = j.template() - o['method'] = 'eth_call' - enc = ABIContractEncoder() - enc.method('token') - enc.typ(ABIContractType.BYTES32) - enc.typ(ABIContractType.UINT256) - enc.bytes32(token_id) - enc.uint256(batch) - data = add_0x(enc.get()) - tx = self.template(sender_address, contract_address) - tx = self.set_code(tx, data) - o['params'].append(self.normalize(tx)) - o['params'].append('latest') - o = j.finalize(o) - return o - - - def get_token(self, contract_address, token_id, sender_address=ZERO_ADDRESS, id_generator=None): - j = JSONRPCRequest(id_generator) - o = j.template() - o['method'] = 'eth_call' - enc = ABIContractEncoder() - enc.method('mintedToken') - enc.typ(ABIContractType.BYTES32) - enc.bytes32(token_id) - data = add_0x(enc.get()) - tx = self.template(sender_address, contract_address) - tx = self.set_code(tx, data) - o['params'].append(self.normalize(tx)) - o['params'].append('latest') - o = j.finalize(o) - return o - - - def get_digest(self, contract_address, token_id, sender_address=ZERO_ADDRESS, id_generator=None): - j = JSONRPCRequest(id_generator) - o = j.template() - o['method'] = 'eth_call' - enc = ABIContractEncoder() - enc.method('getDigest') - enc.typ(ABIContractType.BYTES32) - enc.bytes32(token_id) - data = add_0x(enc.get()) - tx = self.template(sender_address, contract_address) - tx = self.set_code(tx, data) - o['params'].append(self.normalize(tx)) - o['params'].append('latest') - o = j.finalize(o) - return o - - - def mint_to(self, contract_address, sender_address, recipient, token_id, batch, index=None, tx_format=TxFormat.JSONRPC): - enc = ABIContractEncoder() - - if index != None: - enc.method('mintExactFromBatchTo') - enc.typ(ABIContractType.ADDRESS) - enc.typ(ABIContractType.BYTES32) - enc.typ(ABIContractType.UINT16) - enc.typ(ABIContractType.UINT48) - enc.address(recipient) - enc.bytes32(token_id) - enc.uintn(batch, 16) - enc.uintn(index, 48) - data = enc.get() - else: - enc.method('mintFromBatchTo') - enc.typ(ABIContractType.ADDRESS) - enc.typ(ABIContractType.BYTES32) - enc.typ(ABIContractType.UINT16) - enc.address(recipient) - enc.bytes32(token_id) - enc.uintn(batch, 16) - data = enc.get() - - tx = self.template(sender_address, contract_address, use_nonce=True) - tx = self.set_code(tx, data) - tx = self.finalize(tx, tx_format) - return tx - - - # def to_index_id(self, token_id, batch, index): - - - @classmethod - def parse_batch_of(self, v): - r = abi_decode_single(ABIContractType.UINT256, v) - if r == INVALID_BATCH: - raise InvalidBatchError() - return r - - - @classmethod - def parse_token_spec(self, v): - v = strip_0x(v) - d = ABIContractDecoder() - d.typ(ABIContractType.UINT48) - d.typ(ABIContractType.UINT48) - d.val(v[:64]) - d.val(v[64:128]) - r = d.decode() - return TokenSpec(r[0], r[1]) - - @classmethod - def parse_token(self, v, token_id): - v = strip_0x(v) - if v == strip_0x(ZERO_CONTENT): - return MintedToken() - - token_id = strip_0x(token_id) - c = v[:2] - addr = v[24:] - if int(c, 16) & 0x40 > 0: - return MintedToken(addr, token_id=token_id, batched=True, minted=True) - - o = MintedToken(addr, minted=True) - o.batch = int(token_id[48:52], 16) - o.index = int(token_id[52:64], 16) - o.token_id = token_id[:48] + v[2:18] - return o diff --git a/python/eth_craft_nft/runnable/dump.py b/python/eth_craft_nft/runnable/dump.py @@ -1,141 +0,0 @@ -"""Deploys badge NFT - -.. moduleauthor:: Louis Holbrook <dev@holbrook.no> -.. pgp:: 0826EDA1702D1E87C6E2875121D2E7BB88C2A746 - -""" - -# SPDX-License-Identifier: GPL-3.0-or-later - -# standard imports -import sys -import os -import json -import argparse -import logging -import time -from enum import Enum - -# external imports -import chainlib.eth.cli -from chainlib.chain import ChainSpec -from chainlib.eth.constant import ZERO_ADDRESS -from chainlib.settings import ChainSettings -from chainlib.eth.settings import process_settings -from chainlib.eth.cli.arg import Arg -from chainlib.eth.cli.arg import ArgFlag -from chainlib.eth.cli.arg import process_args -from chainlib.eth.cli.log import process_log -from chainlib.eth.cli.config import Config -from chainlib.eth.cli.config import process_config -from hexathon import strip_0x -from hexathon import add_0x - -# local imports -from eth_craft_nft import CraftNFT - -logg = logging.getLogger() - -def process_config_local(config, arg, args, flags): - contract = None - try: - contract = config.get('_EXEC_ADDRESS') - except KeyError: - pass - - if contract == None: - address = config.get('_POSARG') - if address: - contract = add_0x(address) - else: - contract = stdin_arg() - - config.add(contract, '_CONTRACT', False) - return config - - -arg_flags = ArgFlag() -arg = Arg(arg_flags) - -flags = arg_flags.STD_READ | arg_flags.EXEC | arg_flags.TAB - -argparser = chainlib.eth.cli.ArgumentParser() -argparser = process_args(argparser, arg, flags) -argparser.add_argument('contract_address', type=str, help='Token contract address (may also be specified by -e)') -args = argparser.parse_args() - -logg = process_log(args, logg) - -config = Config() -config = process_config(config, arg, args, flags, positional_name='contract_address') -config = process_config_local(config, arg, args, flags) -logg.debug('config loaded:\n{}'.format(config)) - -settings = ChainSettings() -settings = process_settings(settings, config) -logg.debug('settings loaded:\n{}'.format(settings)) - - -def render_token_batches(c, conn, token_address, token_id, w=sys.stdout): - i = 0 - while True: - o = c.get_token_spec(token_address, token_id, i) - r = None - try: - r = conn.do(o) - except: - break - spec = c.parse_token_spec(r) - - for j in range(spec.cursor): - cursor_hex = j.to_bytes(6, byteorder='big').hex() - batch_hex = i.to_bytes(2, byteorder='big').hex() - token_id_indexed = token_id[:48] + batch_hex + cursor_hex - render_token_mint(c, conn, token_address, token_id_indexed, w=w) - - i += 1 - - -def render_token_mint(c, conn, token_address, token_id, w=sys.stdout): - o = c.get_token(token_address, token_id) - r = conn.do(o) - token = c.parse_token(r, token_id) - if token.minted: - w.write('token {}\n'.format(token)) - - -def render_token(c, conn, token_address, token_id, w=sys.stdout): - token_id = strip_0x(token_id) - o = c.get_token_spec(token_address, token_id, 0) - r = conn.do(o) - spec = c.parse_token_spec(r) - if spec.count > 0: - return render_token_batches(c, conn, token_address, token_id, w=sys.stdout) - - return render_token_mint(c, conn, token_address, token_id, w=w) - - -def main(): - token_address = config.get('_CONTRACT') - conn = settings.get('CONN') - c = CraftNFT( - chain_spec=settings.get('CHAIN_SPEC'), - gas_oracle=settings.get('GAS_ORACLE'), - ) - - outkeys = config.get('_OUTARG') - - i = 0 - while True: - o = c.token_at(token_address, i) - r = None - try: - r = conn.do(o) - except: - break - render_token(c, conn, token_address, r) - i += 1 - - -if __name__ == '__main__': - main() diff --git a/python/eth_craft_nft/runnable/publish.py b/python/eth_craft_nft/runnable/publish.py @@ -1,127 +0,0 @@ -"""Deploys badge NFT - -.. moduleauthor:: Louis Holbrook <dev@holbrook.no> -.. pgp:: 0826EDA1702D1E87C6E2875121D2E7BB88C2A746 - -""" - -# SPDX-License-Identifier: GPL-3.0-or-later - -# standard imports -import sys -import os -import json -import argparse -import logging -import time -from enum import Enum - -# external imports -import chainlib.eth.cli -from chainlib.chain import ChainSpec -from chainlib.eth.constant import ZERO_ADDRESS -from chainlib.settings import ChainSettings -from chainlib.eth.settings import process_settings -from chainlib.eth.cli.arg import Arg -from chainlib.eth.cli.arg import ArgFlag -from chainlib.eth.cli.arg import process_args -from chainlib.eth.cli.log import process_log -from chainlib.eth.cli.config import Config -from chainlib.eth.cli.config import process_config - -# local imports -from eth_craft_nft import CraftNFT - -logg = logging.getLogger() - - -def process_config_local(config, arg, args, flags): - config.add(args.name, '_TOKEN_NAME', False) - config.add(args.symbol, '_TOKEN_SYMBOL', False) - return config - - -arg_flags = ArgFlag() -arg = Arg(arg_flags) -flags = arg_flags.STD_WRITE | arg_flags.WALLET | arg_flags.CREATE | arg_flags.VALUE | arg_flags.TAB - -argparser = chainlib.eth.cli.ArgumentParser() -argparser.add_argument('--name', type=str, required=True, help='Token name') -argparser.add_argument('--symbol', type=str, required=True, help='Token symbol') -argparser = process_args(argparser, arg, flags) -args = argparser.parse_args(sys.argv[1:]) - -logg = process_log(args, logg) - -config = Config() -config = process_config(config, arg, args, flags) -config = process_config_local(config, arg, args, flags) -logg.debug('config loaded:\n{}'.format(config)) - -settings = ChainSettings() -settings = process_settings(settings, config) -logg.debug('settings loaded:\n{}'.format(settings)) - - -#arg_flags = chainlib.eth.cli.argflag_std_write -#argparser = chainlib.eth.cli.ArgumentParser(arg_flags) -#argparser.add_argument('--name', dest='token_name', type=str, help='Token name') -#argparser.add_argument('--symbol', dest='token_symbol', type=str, help='Token symbol') -#args = argparser.parse_args() -# -#extra_args = { -# 'token_name': None, -# 'token_symbol': None, -# } -#config = chainlib.eth.cli.Config.from_args(args, arg_flags, extra_args=extra_args, default_fee_limit=CraftNFT.gas()) - -#wallet = chainlib.eth.cli.Wallet() -#wallet.from_config(config) - -#rpc = chainlib.eth.cli.Rpc(wallet=wallet) -#conn = rpc.connect_by_config(config) - -#chain_spec = ChainSpec.from_chain_str(config.get('CHAIN_SPEC')) - - -def main(): - #signer = rpc.get_signer() - #signer_address = rpc.get_sender_address() - - token_name = config.get('_TOKEN_NAME') - token_symbol = config.get('_TOKEN_SYMBOL') - conn = settings.get('CONN') - -# gas_oracle = rpc.get_gas_oracle() -# nonce_oracle = rpc.get_nonce_oracle() - - c = CraftNFT( - settings.get('CHAIN_SPEC'), - signer=settings.get('SIGNER'), - gas_oracle=settings.get('FEE_ORACLE'), - nonce_oracle=settings.get('NONCE_ORACLE') - ) - - (tx_hash_hex, o) = c.constructor( - settings.get('SENDER_ADDRESS'), - token_name, - token_symbol, - ) - if config.get('_RPC_SEND'): - conn.do(o) - if config.true('_WAIT'): - r = conn.wait(tx_hash_hex) - if r['status'] == 0: - sys.stderr.write('EVM revert while deploying contract. Wish I had more to tell you') - sys.exit(1) - # TODO: pass through translator for keys (evm tester uses underscore instead of camelcase) - address = r['contractAddress'] - - print(address) - else: - print(tx_hash_hex) - else: - print(o) - -if __name__ == '__main__': - main() diff --git a/python/requirements.txt b/python/requirements.txt @@ -1 +1,2 @@ eth-erc721~=0.0.4 +aenum~=3.1.11 diff --git a/python/setup.cfg b/python/setup.cfg @@ -0,0 +1,37 @@ +[metadata] +name = craft-nft +version = 0.0.1 +description = A standalone NFT implementation for real-world arts and crafts assets +author = Louis Holbrook +author_email = dev@holbrook.no +url = https://git.defalslfy.org/craft-nft,git +keywords = + dlt + blockchain + cryptocurrency + ethereum +classifiers = + Programming Language :: Python :: 3 + Operating System :: OS Independent + Development Status :: 3 - Alpha + Topic :: Software Development :: Libraries + Environment :: Console + Intended Audience :: Developers + License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+) + Topic :: Internet +# Topic :: Blockchain :: EVM +license = AGPLv3+ +licence_files = + LICENSE + +[options] +include_package_data = True +python_requires = >= 3.7 +packages = + craft_nft + craft_nft.runnable + +[options.entry_points] +console_scripts = + craftnft-publish = craft_nft.runnable.publish:main + craftnft-dump = craft_nft.runnable.dump:main diff --git a/python/setup.py b/python/setup.py @@ -0,0 +1,26 @@ +from setuptools import setup +import os + + +requirements = [] +f = open('requirements.txt', 'r') +while True: + l = f.readline() + if l == '': + break + requirements.append(l.rstrip()) +f.close() + +test_requirements = [] +f = open('test_requirements.txt', 'r') +while True: + l = f.readline() + if l == '': + break + test_requirements.append(l.rstrip()) +f.close() + +setup( + install_requires=requirements, + tests_require=test_requirements, + ) diff --git a/python/tests/test_basic.py b/python/tests/test_basic.py @@ -27,12 +27,12 @@ from hexathon import ( ) from chainlib.eth.tx import TxFormat from chainlib.eth.contract import ABIContractEncoder -from chainlib.eth.contract import ABIContractType # local imports -from eth_craft_nft import CraftNFT -from eth_craft_nft.error import InvalidBatchError +from craft_nft import CraftNFT +from craft_nft.error import InvalidBatchError +from craft_nft.eth import ABIContractType logging.basicConfig(level=logging.DEBUG) logg = logging.getLogger() diff --git a/python/tests/test_numbered.py b/python/tests/test_numbered.py @@ -31,8 +31,8 @@ from chainlib.eth.contract import ABIContractType # local imports -from eth_craft_nft import CraftNFT -from eth_craft_nft.error import InvalidBatchError +from craft_nft import CraftNFT +from craft_nft.error import InvalidBatchError logging.basicConfig(level=logging.DEBUG) logg = logging.getLogger() diff --git a/python/tests/test_spec.py b/python/tests/test_spec.py @@ -31,8 +31,8 @@ from chainlib.eth.contract import ABIContractType # local imports -from eth_craft_nft import CraftNFT -from eth_craft_nft.error import InvalidBatchError +from craft_nft import CraftNFT +from craft_nft.error import InvalidBatchError logging.basicConfig(level=logging.DEBUG) logg = logging.getLogger() @@ -76,7 +76,6 @@ class Test(EthTesterCase): r = self.rpc.do(o) spec = c.parse_token_spec(r) self.assertEqual(spec.count, 3) - self.assertEqual(spec.cumulative_count, 5) self.assertEqual(spec.cursor, 1) diff --git a/python/tests/test_supply.py b/python/tests/test_supply.py @@ -30,8 +30,8 @@ from chainlib.eth.contract import ABIContractType # local imports -from eth_craft_nft import CraftNFT -from eth_craft_nft.error import InvalidBatchError +from craft_nft import CraftNFT +from craft_nft.error import InvalidBatchError logging.basicConfig(level=logging.DEBUG) logg = logging.getLogger() diff --git a/solidity/Makefile b/solidity/Makefile @@ -6,7 +6,7 @@ all: $(SOLC) --abi CraftNFT.sol --evm-version byzantium | awk 'NR>3' > CraftNFT.json install-py: all - cp -v CraftNFT*{json,bin} ../python/eth_craft_nft/data/ + cp -v CraftNFT*{json,bin} ../python/craft_nft/data/ install-js: all cp -v CraftNFT*{json,bin} ../js/contract/