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 };