craft-nft

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

commit 46a7cbf3f7606285c45437318608b3d725fc5298
parent bfe4d8ba394f40c505b4c6e44fcc386d6d77aa44
Author: lash <dev@holbrook.no>
Date:   Mon, 19 Dec 2022 18:14:49 +0000

Add readme, demo script

Diffstat:
MREADME.md | 109+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Mjs/index.html | 2+-
Ajs/settings-template.sh | 4++++
Mpython/craft_nft/nft.py | 24++++++++++++++++++++----
Mpython/craft_nft/runnable/allocate.py | 13++++++-------
Mpython/craft_nft/runnable/dump.py | 5++---
Mpython/craft_nft/runnable/mint.py | 21+++++++++++++--------
Apython/demo.sh | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpython/requirements.txt | 2+-
Mpython/tests/test_basic.py | 21+++++++++++++++++++++
10 files changed, 225 insertions(+), 29 deletions(-)

diff --git a/README.md b/README.md @@ -39,10 +39,6 @@ The above will compile the smart contract, and copy it to the python and js envi ## Publishing the token contract ``` -$ pip install craft-nft - -... or ... - $ pip install $REPO_ROOT/dist/craft-nft-x.x.x.tar.gz ``` @@ -64,6 +60,91 @@ cd python PYTHONPATH=. python craft_nft/runnable/publish.py <args> ``` +## Allocating and minting tokens + +There are CLI tools for allocating, minting and listing tokens. Here is a full example for tokens based on nonsensical token data: + +``` +set +e +# chainlib required settings, edit as needed. +export RPC_PROVIDER=http://localhost:8545 +# The chain spec 3rd field MUST match the chain id of the newtork +export CHAIN_SPEC=evm:kitabu:5050:test +# this file will be used to sign all transcations below +# it must have sufficient gas token balance +export WALLET_KEY_FILE=${WALLET_KEY_FILE:-alice.json} + +# eth-keyfile is provided by the funga-eth module, a dependency of craft-nft +>&2 echo generating keys... +eth-keyfile -z > bob.json +eth-keyfile -z > carol.json +eth-keyfile -z > dave.json +export ALICE=$(eth-keyfile -z -d $WALLET_KEY_FILE) +>&2 echo "Alice has key $ALICE. This key will be used for signing" +export BOB=$(eth-keyfile -z -d bob.json) +>&2 echo Bob has key $BOB +export CAROL=$(eth-keyfile -z -d carol.json) +>&2 echo Carol has key $CAROL +export DAVE=$(eth-keyfile -z -d dave.json) +>&2 echo Dave has key $DAVE + +# publish contract +>&2 echo publishing token ... +echo "description missing" > description.txt +craftnft-publish --name "Test Token" --symbol "TEST" --declaration-file description.txt -s -w > token.txt +export TOKEN_ADDRESS=$(cat token.txt | eth-checksum) +>&2 echo published token $TOKEN_ADDRESS + +token_foo=$(echo -n foo | sha256sum | awk '{print $1;}') +>&2 echo allocating unique token "foo" ... +craftnft-allocate -e $TOKEN_ADDRESS -s -w $token_foo >> txs.txt +token_bar=$(echo -n bar | sha256sum | awk '{print $1;}') +>&2 echo allocating batched token "bar" ... +craftnft-allocate -e $TOKEN_ADDRESS -s -w --count 10 $token_bar >> txs.txt + +>&2 echo minting the "foo" token to Alice ... +craftnft-mint -e $TOKEN_ADDRESS --token-id $token_foo -s -w $ALICE >> txs.txt +>&2 echo minting a "bar" token to Bob ... +craftnft-mint -e $TOKEN_ADDRESS --token-id $token_bar -s -w $BOB >> txs.txt +>&2 echo minting a "bar" token to Carol ... +craftnft-mint -e $TOKEN_ADDRESS --token-id $token_bar -s -w $CAROL >> txs.txt +>&2 echo minting a "bar" token to Alice ... +craftnft-mint -e $TOKEN_ADDRESS --token-id $token_bar -s -w $ALICE >> txs.txt + +# erc721-tranfser is provided by the eth-erc721 module, a dependency of craft-nft +# It is a generic tool, so we need to specify the gas budget manually +>&2 echo "transfer Alice's bar token to Dave ..." +erc721-transfer -e $TOKEN_ADDRESS -a $DAVE -s -w --fee-limit 100000 0xfcde2b2edba56bf408601fb721fe9b5c338d10ee429ea04f0000000000000002 >> txs.txt + +craftnft-dump $TOKEN_ADDRESS +set -e +``` + +The above code is stored in demo.sh. A bit of editing is needed to set up according to your environment and signing keys. + +The outputs of a sample run should look something like this: + +``` +sh /home/lash/src/home/eth/craft-nft/python/demo.sh +generating keys... +Alice has key Eb3907eCad74a0013c259D5874AE7f22DcBcC95C. This key will be used for signing +Bob has key e5E6656181108cCCB222243e1896bC0D0328af3B +Carol has key 9aB2C0f01CA7135a106829EFfBb2B303191352b3 +Dave has key A952a6e57A45744a924B7bA5cC4Fbb42438EaEA5 +publishing token ... +published token 7115070486ce22004D63D70f62F52175cedB3bAd +allocating unique token foo ... +allocating batched token bar ... +minting the foo token to Alice ... +minting a bar token to Bob ... +minting a bar token to Carol ... +minting a bar token to Alice ... +transfer Alice's bar token to Dave ... +token 2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae owned by Eb3907eCad74a0013c259D5874AE7f22DcBcC95C +token fcde2b2edba56bf408601fb721fe9b5c338d10ee429ea04f0000000000000000 owned by e5E6656181108cCCB222243e1896bC0D0328af3B - (id fcde2b2edba56bf408601fb721fe9b5c338d10ee429ea04fae5511b68fbf8fb9 batch 0 index 0) +token fcde2b2edba56bf408601fb721fe9b5c338d10ee429ea04f0000000000000001 owned by 9aB2C0f01CA7135a106829EFfBb2B303191352b3 - (id fcde2b2edba56bf408601fb721fe9b5c338d10ee429ea04fae5511b68fbf8fb9 batch 0 index 1) +token fcde2b2edba56bf408601fb721fe9b5c338d10ee429ea04f0000000000000002 owned by A952a6e57A45744a924B7bA5cC4Fbb42438EaEA5 - (id fcde2b2edba56bf408601fb721fe9b5c338d10ee429ea04fae5511b68fbf8fb9 batch 0 index 2) +``` ## Using the javascript browser UI example. @@ -73,6 +154,7 @@ In the `js` directory, create a file called `settings.json` with the following c ``` { "contract": "<address of published contract>" + "contentGatewayUrl": null } ``` @@ -84,7 +166,7 @@ cd js webfsd -F -d -p <port> ``` -## Content addressed server +## Content addressed storage The example browser application uses the Wala service for content addressed storage and retrieval. The application will still work without the service, but some data will not be available for display. @@ -101,3 +183,20 @@ target/release/wala -p <port> ``` For the example browser application to use the service, the `wala` url needs to be added to the `contentGatewayUrl` field of the settings.json file in the `js` directory. + +### Data published to storage + +The data in content-addressed storage used by the application is: + +* The contract declaration (read) +* Token declarations (read/write) + +See the `$REPO_ROOT/doc/latex/terminology.latex` document for a terminology overview. + + +## Further reading + +For more details on the chainlib/chaintool contents, please refer to the [chaintool documentation repository](https://git.defalsify.org/chaintool-doc). + +All chaintool related code repositories are hosted on [https://git.defalsify.org](https://git.defalsify.org) + diff --git a/js/index.html b/js/index.html @@ -38,7 +38,7 @@ a:hover { <dd id="data_symbol"></dd> <dt>supply</dt> <dd id="data_supply"></dd> - <dt>declaration</dt> + <dt>contract declaration</dt> <dd id="data_declaration"></dd> </dl> </dd> diff --git a/js/settings-template.sh b/js/settings-template.sh @@ -0,0 +1,4 @@ +{ + "contract": "0x0000000000000000000000000000000000000000", + "contentGatewayUrl": null +} diff --git a/python/craft_nft/nft.py b/python/craft_nft/nft.py @@ -26,6 +26,17 @@ INVALID_BATCH = (2**256)-1 logg = logging.getLogger(__name__) + +def to_batch_key(token_id, batch, index): + token_id = strip_0x(token_id) + if len(token_id) != 64: + raise ValueError('token id must be 32 bytes') + token_id = token_id[:48] + token_id += batch.to_bytes(2, byteorder='big').hex() + token_id += index.to_bytes(6, byteorder='big').hex() + return token_id + + class TokenSpec: def __init__(self, count, cursor): @@ -51,15 +62,17 @@ class MintedToken: def __str__(self): owner = to_checksum_address(self.owner) if self.batched: - return '{} owned {}'.format( + return '{} owned by {}'.format( self.token_id, owner, ) - return '{} batch {} index {} owned by {}'.format( + token_key = to_batch_key(self.token_id, self.batch, self.index) + return '{} owned by {} - (id {} batch {} index {})'.format( + token_key, + owner, self.token_id, self.batch, self.index, - owner, ) @@ -91,7 +104,9 @@ class CraftNFT(ERC721): return 4000000 - def constructor(self, sender_address, name, symbol, declaration, tx_format=TxFormat.JSONRPC): + def constructor(self, sender_address, name, symbol, declaration=None, tx_format=TxFormat.JSONRPC): + if declaration == None: + declaration = strip_0x(ZERO_CONTENT) code = CraftNFT.bytecode() enc = ABIContractEncoder() enc.string(name) @@ -153,6 +168,7 @@ class CraftNFT(ERC721): 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() diff --git a/python/craft_nft/runnable/allocate.py b/python/craft_nft/runnable/allocate.py @@ -45,8 +45,11 @@ def process_config_local(config, arg, args, flags): assert args.count < 2**48 config.add(args.count, '_TOKEN_COUNT', False) - return config + if args.fee_limit == None: + config.add(200000, '_FEE_LIMIT', True) + + return config arg_flags = ArgFlag() arg = Arg(arg_flags) @@ -54,7 +57,7 @@ flags = arg_flags.STD_WRITE | arg_flags.CREATE | arg_flags.VALUE | arg_flags.TAB argparser = chainlib.eth.cli.ArgumentParser() argparser = process_args(argparser, arg, flags) -argparser.add_argument('--count', default=0, type=int, required=True, help='Amount of tokens in batch') +argparser.add_argument('--count', default=0, type=int, help='Amount of tokens in batch') argparser.add_argument('token_id', type=str, nargs='*', help='token id: sha256 sum of token data, in hex') args = argparser.parse_args(sys.argv[1:]) @@ -96,12 +99,8 @@ def main(): 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) + print(tx_hash_hex) else: print(o) diff --git a/python/craft_nft/runnable/dump.py b/python/craft_nft/runnable/dump.py @@ -33,6 +33,7 @@ from hexathon import add_0x # local imports from craft_nft import CraftNFT +from craft_nft.nft import to_batch_key logg = logging.getLogger() @@ -90,9 +91,7 @@ def render_token_batches(c, conn, token_address, token_id, w=sys.stdout): 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 + token_id_indexed = to_batch_key(token_id, i, j) render_token_mint(c, conn, token_address, token_id_indexed, w=w) i += 1 diff --git a/python/craft_nft/runnable/mint.py b/python/craft_nft/runnable/mint.py @@ -30,6 +30,7 @@ from chainlib.eth.cli.log import process_log from chainlib.eth.cli.config import Config from chainlib.eth.cli.config import process_config from chainlib.eth.constant import ZERO_CONTENT +from chainlib.eth.address import to_checksum_address from hexathon import strip_0x # local imports @@ -44,12 +45,20 @@ def process_config_local(config, arg, args, flags): config.add(token_id, '_TOKEN_ID', False) config.add(args.batch, '_TOKEN_BATCH', False) + + if args.fee_limit == None: + config.add(200000, '_FEE_LIMIT', True) + return config def process_settings_local(settings, config): settings.set('TOKEN_ID', config.get('_TOKEN_ID')) + if config.get('_POSARG') != None: + recipient = to_checksum_address(config.get('_POSARG')) + settings.set('RECIPIENT', recipient) + if (config.get('_TOKEN_BATCH') != None): settings.set('TOKEN_BATCH', config.get('_TOKEN_BATCH')) return settings @@ -85,12 +94,13 @@ argparser = process_args(argparser, arg, flags) argparser.add_argument('--token-id', type=str, required=True, help='Token id to mint from') argparser.add_argument('--check', action='store_true', help='Only check whether a token can be minted') argparser.add_argument('--batch', type=int, help='Mint from the given batch. If not specified, the first mintable batch will be used') +argparser.add_argument('token_recipient', type=str, nargs='*', help='Recipient address') args = argparser.parse_args(sys.argv[1:]) logg = process_log(args, logg) config = Config() -config = process_config(config, arg, args, flags, positional_name='token_id') +config = process_config(config, arg, args, flags, positional_name='token_recipient') config = process_config_local(config, arg, args, flags) logg.debug('config loaded:\n{}'.format(config)) @@ -98,7 +108,7 @@ settings = ChainSettings() settings = process_settings(settings, config) settings = process_settings_local(settings, config) logg.debug('settings loaded:\n{}'.format(settings)) -exit + def main(): conn = settings.get('CONN') @@ -125,12 +135,7 @@ def main(): 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) + print(tx_hash_hex) else: print(o) diff --git a/python/demo.sh b/python/demo.sh @@ -0,0 +1,53 @@ +set +e +# chainlib required settings, edit as needed. +export RPC_PROVIDER=http://localhost:8545 +# The chain spec 3rd field MUST match the chain id of the newtork +export CHAIN_SPEC=evm:kitabu:5050:test +# this file will be used to sign all transcations below +# it must have sufficient gas token balance +export WALLET_KEY_FILE=${WALLET_KEY_FILE:-alice.json} + +# eth-keyfile is provided by the funga-eth module, a dependency of craft-nft +>&2 echo generating keys... +eth-keyfile -z > bob.json +eth-keyfile -z > carol.json +eth-keyfile -z > dave.json +export ALICE=$(eth-keyfile -z -d $WALLET_KEY_FILE) +>&2 echo "Alice has key $ALICE. This key will be used for signing" +export BOB=$(eth-keyfile -z -d bob.json) +>&2 echo Bob has key $BOB +export CAROL=$(eth-keyfile -z -d carol.json) +>&2 echo Carol has key $CAROL +export DAVE=$(eth-keyfile -z -d dave.json) +>&2 echo Dave has key $DAVE + +# publish contract +>&2 echo publishing token ... +echo "description missing" > description.txt +craftnft-publish --name "Test Token" --symbol "TEST" --declaration-file description.txt -s -w > token.txt +export TOKEN_ADDRESS=$(cat token.txt | eth-checksum) +>&2 echo published token $TOKEN_ADDRESS + +token_foo=$(echo -n foo | sha256sum | awk '{print $1;}') +>&2 echo allocating unique token "foo" ... +craftnft-allocate -e $TOKEN_ADDRESS -s -w $token_foo >> txs.txt +token_bar=$(echo -n bar | sha256sum | awk '{print $1;}') +>&2 echo allocating batched token "bar" ... +craftnft-allocate -e $TOKEN_ADDRESS -s -w --count 10 $token_bar >> txs.txt + +>&2 echo minting the "foo" token to Alice ... +craftnft-mint -e $TOKEN_ADDRESS --token-id $token_foo -s -w $ALICE >> txs.txt +>&2 echo minting a "bar" token to Bob ... +craftnft-mint -e $TOKEN_ADDRESS --token-id $token_bar -s -w $BOB >> txs.txt +>&2 echo minting a "bar" token to Carol ... +craftnft-mint -e $TOKEN_ADDRESS --token-id $token_bar -s -w $CAROL >> txs.txt +>&2 echo minting a "bar" token to Alice ... +craftnft-mint -e $TOKEN_ADDRESS --token-id $token_bar -s -w $ALICE >> txs.txt + +# erc721-tranfser is provided by the eth-erc721 module, a dependency of craft-nft +# It is a generic tool, so we need to specify the gas budget manually +>&2 echo "transfer Alice's bar token to Dave ..." +erc721-transfer -e $TOKEN_ADDRESS -a $DAVE -s -w --fee-limit 100000 0xfcde2b2edba56bf408601fb721fe9b5c338d10ee429ea04f0000000000000002 >> txs.txt + +craftnft-dump $TOKEN_ADDRESS +set -e diff --git a/python/requirements.txt b/python/requirements.txt @@ -1,2 +1,2 @@ -eth-erc721~=0.0.4 +eth-erc721~=0.0.5 aenum~=3.1.11 diff --git a/python/tests/test_basic.py b/python/tests/test_basic.py @@ -299,6 +299,27 @@ class Test(EthTesterCase): self.assertTrue(is_same_address(owner[24:], self.accounts[2])) + def test_transfer_batched(self): + nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc) + c = CraftNFT(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle) + + (tx_hash_hex, o) = c.allocate(self.address, self.accounts[0], hash_of_foo, amount=10) + self.rpc.do(o) + + (tx_hash_hex, o) = c.mint_to(self.address, self.accounts[0], self.accounts[1], hash_of_foo, 0) + self.rpc.do(o) + + expected_id = hash_of_foo[:64-16] + '0000000000000000' + int_of_foo = int(expected_id, 16) + + nonce_oracle = RPCNonceOracle(self.accounts[1], self.rpc) + c = CraftNFT(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle) + (tx_hash, o) = c.transfer_from(self.address, self.accounts[1], self.accounts[1], self.accounts[2], int_of_foo) + self.rpc.do(o) + o = receipt(tx_hash_hex) + r = self.conn.do(o) + self.assertEqual(r['status'], 1) + def test_fill_batches(self): nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)