craft-nft

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

engine.js (8951B)


      1 const Web3 = require('web3');
      2 import MetaMaskSDK from '@metamask/sdk';
      3 
      4 /**
      5  * Loads a chosen provider for the w3 inteface. Currently hard-coded to Metamask.
      6  *
      7  * @return {Object} Provider
      8  */
      9 function loadProvider() {
     10 	const mm = new MetaMaskSDK({injectProvider: false});
     11 	const w3_provider = mm.getProvider();
     12 	w3_provider.request({method: 'eth_requestAccounts'});
     13 	return w3_provider;
     14 }
     15 
     16 /**
     17  * Returns a new web3 client instance.
     18  *
     19  * @return {Object} client
     20  */
     21 function loadConn(provider) {
     22 	const w3 = new Web3(provider);
     23 	return w3;
     24 }
     25 
     26 /**
     27  * Instantiates the token smart contract using the web3 client instance.
     28  *
     29  * @param {Object} client
     30  * @param {Object} config
     31  */
     32 function loadContract(w3, config) {
     33 	const contract = new w3.eth.Contract(config.abi, config.contract);	
     34 	return contract;
     35 }
     36 
     37 
     38 /**
     39  * Initialize the session object using config and client.
     40  *
     41  * Calls runner with client and session when initialization has been completed.
     42  *
     43  * @param {Object} client
     44  * @param {Object} config
     45  * @param {Object} session
     46  * @param {Function} runner
     47  * @throws free-form If contract cannot be loaded, or contract interface does not meet expectations.
     48  */
     49 async function startSession(w3, config, session, runner) {
     50 	const acc = await w3.eth.getAccounts();
     51 	session.account = acc[0];
     52 	session.contractAddress = config.contract;
     53 	session.contentGatewayUrl = config.contentGatewayUrl;
     54 	session.contract = loadContract(w3, config);
     55 	session.name = await session.contract.methods.name().call({from: session.account});
     56 	session.symbol = await session.contract.methods.symbol().call({from: session.account});
     57 	session.supply = await session.contract.methods.totalSupply().call({from: session.account});
     58 	//session.declarationHash = await session.contract.methods.declaration().call({from: session.account});
     59 	//if (session.declarationHash.substring(0,2) == '0x') {
     60 	//	session.declarationHash = session.declarationHash.substring(2);
     61 	//}
     62 	runner(w3, session);
     63 }
     64 
     65 
     66 /**
     67  * Reload session with current states.
     68  *
     69  * @param {Object} session
     70  * @return {Object} session (refreshed)
     71  */
     72 async function refreshSession(session) {
     73 	session.supply = await session.contract.methods.totalSupply().call({from: session.account});
     74 	return session;
     75 }
     76 
     77 
     78 /**
     79  * Visits callback with token spec as argument for every allocated token.
     80  *
     81  * @param {Object} client
     82  * @param {Object} session
     83  * @param {Function} callback
     84  * @throws free-form If token does not exist
     85  */
     86 async function getTokens(w3, session, callback) {
     87 	let i = 0;
     88 	while (true) {
     89 		let token = undefined;
     90 		try {
     91 			token = await session.contract.methods.tokens(i).call({from: session.account});
     92 			callback(token);
     93 		} catch(e) {
     94 			break;
     95 		};
     96 		i++;
     97 	}
     98 }
     99 
    100 
    101 /**
    102  * Create a new token allocation. Refer to the smart contract function allocate() for further details.
    103  *
    104  * @param {Object} session
    105  * @param {String} tokenId (hex)
    106  * @param {Number} amount
    107  * @throws free-form If transaction is refused by the client
    108  */
    109 async function allocateToken(session, tokenId, amount) {
    110 	session.contract.methods.allocate('0x' + tokenId, amount).send({
    111 		from: session.account,
    112 		value: 0,
    113 	});
    114 }
    115 
    116 
    117 /**
    118  * Mint a new token from an existing allocation. Refer to the smart contract function mintFromBatchTo() for further details.
    119  *
    120  * @param {Object} session
    121  * @param {String} tokenId (hex)
    122  * @param {Number} batch
    123  * @param {String} recipient of token mint
    124  * @throws free-form If transaction is refused by the client
    125  */
    126 async function mintToken(session, tokenId, batch, recipient, index) {
    127 	const w3 = new Web3();
    128 	const address = await w3.utils.toChecksumAddress(recipient);
    129 	if (index === undefined || isNaN(index)) {
    130 		session.contract.methods.mintFromBatchTo(address, '0x' + tokenId, batch).send({
    131 			from: session.account,
    132 			value: 0,
    133 		});
    134 	} else {
    135 		session.contract.methods.mintExactFromBatchTo(address, '0x' + tokenId, batch, index).send({
    136 			from: session.account,
    137 			value: 0,
    138 		});
    139 	}
    140 }
    141 
    142 
    143 /**
    144  * Assemble and return data describing a single minted token.
    145  *
    146  * @param {Object} session
    147  * @psram {String} tokenId (hex)
    148  * @param {Number} batch
    149  * @return {Object} 
    150  * @throws free-form if token does not exist
    151  */
    152 async function getMintedToken(session, tokenId, batch) {
    153 	let o = {
    154 		mintable: false,
    155 		single: false,
    156 		cap: 0,
    157 		count: 0,
    158 		sparse: false,
    159 	}
    160 	let token = await session.contract.methods.token('0x' + tokenId, batch).call({from: session.account});
    161 	if (token === undefined) {
    162 		return o;
    163 	}
    164 	if (batch == 0) {
    165 		if (token.count == 0) {
    166 			o.cap = 1;
    167 			o.count = parseInt(token.cursor);
    168 			if (token.cursor == 0) {
    169 				o.mintable = true;
    170 			}
    171 			o.single = true;
    172 			return o;
    173 		}
    174 	}
    175 	o.sparse = token.sparse;
    176 	o.cap = parseInt(token.count);
    177 	o.count = parseInt(token.cursor);
    178 	if (o.count < o.cap) {
    179 		o.mintable = true;
    180 	}
    181 	return o;
    182 }
    183 
    184 
    185 /**
    186  * Generate a Token Id from a resolved Token Key.
    187  *
    188  * In the case of a Unique Token, this will be the same string.
    189  *
    190  * In case of a Batched Token, this will replace the batch and index embedded in the key with the remainder of the Token Id hash.
    191  *
    192  * @param {Object} session
    193  * @param {String} tokenId (hex)
    194  * @param {String} tokenContent (hex)
    195  * @throws free-form If token does not exist
    196  * @todo Function is a bit long, could be shortened.
    197  */
    198 async function toToken(session, tokenId, tokenContent) {
    199 	if (tokenId.substring(0, 2) == '0x') {
    200 		tokenId = tokenId.substring(2);
    201 	}
    202 
    203 	if (tokenContent.substring(0, 2) == '0x') {
    204 		tokenContent = tokenContent.substring(2);
    205 	}
    206 	
    207 	let data = {
    208 		tokenId: tokenId,
    209 		minted: false,
    210 		mintedTokenId: undefined,
    211 		owner: undefined,
    212 		issue: undefined,
    213 		batches: undefined,
    214 		sparse: false,
    215 	};
    216 
    217 	let issue = undefined;
    218 
    219 	// check whether it is an active minted token, and whether it's unique of batched.
    220 	// if not active we stop processing here.
    221 	const v = parseInt(tokenContent.substring(0, 2), 16);
    222 	if ((v & 0x80) == 0) {
    223 		// execute this only if token is batched.
    224 		if ((v & 0x40) == 0) {
    225 			issue = {};
    226 			const state = await getBatches(session, tokenId);
    227 			data.batches = state.batches;
    228 			issue.cap = state.cap;
    229 			issue.count = state.count;
    230 			data.issue = issue;
    231 		}
    232 		return data;	
    233 	} 
    234 
    235 	data.minted = true;
    236 
    237 	// Fill in stats as applicable to whether Unique or Batched.
    238 	let k = tokenId;
    239 	issue = {}
    240 	if ((v & 0x40) == 0) {
    241 		k = tokenId.substring(0, 48) + tokenContent.substring(2, 18);
    242 		issue.batch = parseInt(tokenId.substring(48, 50), 16);
    243 		issue.index = parseInt(tokenId.substring(50, 64), 16);
    244 
    245 		data.cap = parseInt(token.count);
    246 		data.count = parseInt(token.cursor);
    247 		data.sparse = token.sparse;
    248 	} else {
    249 		data.batches = 0;
    250 		issue.cap = 1;
    251 		issue.count = 1;
    252 		data.issue = issue;	
    253 	}
    254 
    255 	data.issue = issue;
    256 	data.tokenId = k;
    257 	data.owner = tokenContent.substring(24);
    258 
    259 	return data;
    260 }
    261 
    262 
    263 /**
    264  * Retrieve current state of data for minted token.
    265  *
    266  * @param {Object} session
    267  * @param {String} tokenId
    268  * @return {Object} token
    269  */
    270 async function getTokenChainData(session, tokenId) {
    271 	const v = await session.contract.methods.mintedToken('0x' + tokenId).call({from: session.account});
    272 	
    273 	const mintedToken = await toToken(session, tokenId, v);
    274 
    275 	return mintedToken;
    276 }
    277 
    278 
    279 /**
    280  * Visit callback with token spec of every allocated token.
    281  *
    282  * @param {Object} session
    283  * @param {String} tokenId (hex)
    284  * @param {Function} callback
    285  * @return {Object} summary of iteration.
    286  */
    287 async function getBatches(session, tokenId, callback) {
    288 	let token = await session.contract.methods.token('0x' + tokenId, 0).call({from: session.account});
    289 	if (token.count == 0 && callback !== undefined) {
    290 		callback(-1);
    291 		return;
    292 	}
    293 
    294 	if (callback !== undefined) {
    295 		callback(0, token.count, token.cursor);
    296 	}
    297 
    298 	let i = 1;
    299 	let count = parseInt(token.cursor);
    300 	let cap = parseInt(token.count);
    301 	while (true) {
    302 		try {
    303 			token = await session.contract.methods.token('0x' + tokenId, i).call({from: session.account});
    304 		} catch(e) {
    305 			break;
    306 		}
    307 		if (callback !== undefined) {
    308 			callback(i, token.count, token.cursor);
    309 		}
    310 		i++;
    311 		count += parseInt(token.cursor);
    312 		cap += parseInt(token.count);
    313 	}
    314 	return {
    315 		batches: i,
    316 		count: count,
    317 		cap: cap,
    318 	};
    319 }
    320 
    321 
    322 /**
    323  * Check if the given address is the owner of the smart contract.
    324  *
    325  * Only the owner may allocate and mint tokens.
    326  *
    327  * @param {Object} session
    328  * @param {String} address (hex)
    329  * @return {Boolean} true if owner
    330  */
    331 async function isOwner(session, address) {
    332 	let owner = await session.contract.methods.owner().call({from: session.account});
    333 
    334 	const w3 = new Web3();
    335 	address = await w3.utils.toChecksumAddress(address);
    336 	owner = await w3.utils.toChecksumAddress(owner);
    337 
    338 	return address == owner;
    339 }
    340 
    341 
    342 module.exports = {
    343 	loadProvider: loadProvider,
    344 	loadConn: loadConn,
    345 	startSession: startSession,
    346 	getTokens: getTokens,
    347 	getBatches: getBatches,
    348 	allocateToken: allocateToken,
    349 	mintToken: mintToken,
    350 	isOwner: isOwner,
    351 	getTokenChainData: getTokenChainData,
    352 	getMintedToken: getMintedToken,
    353 };