UNPKG

@zkpass/transgate-js-sdk

Version:

<p align="center"> <img src="assets/logo.png" width="300" alt="transgate-js-sdk.js" /> </p>

512 lines (511 loc) 23.3 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const web3_1 = __importDefault(require("web3")); const buffer_1 = require("buffer"); const secp256k1_1 = __importDefault(require("secp256k1")); const borsh = __importStar(require("borsh")); const js_sha3_1 = __importDefault(require("js-sha3")); const ton_1 = require("@ton/ton"); const qrcode_1 = __importDefault(require("qrcode")); const constants_1 = require("./constants"); const types_1 = require("./types"); const error_1 = require("./error"); const solanaInstruction_1 = require("./solanaInstruction"); const helper_1 = require("./helper"); const crypto_1 = require("@ton/crypto"); class TransgateConnect { constructor(appid) { this.appid = appid; this.baseServer = constants_1.server; this.terminal = false; } async launch(schemaId, address) { return await this.runTransgate({ schemaId, address, chainType: 'evm' }); } async launchWithSolana(schemaId, address) { return await this.runTransgate({ schemaId, address, chainType: 'sol' }); } async launchWithTon(schemaId, address) { return await this.runTransgate({ schemaId, address, chainType: 'ton' }); } async runTransgate({ schemaId, address, chainType = 'evm', }) { this.terminal = false; const device = (0, helper_1.getDeviceType)(); if (device === 'iOS') { this.handleIOSModal(); } this.transgateAvailable = await this.isTransgateAvailable(); const config = await this.requestConfig(); if (config.schemas.findIndex((schema) => schema.schema_id === schemaId) === -1) { throw new error_1.TransgateError(error_1.ErrorCode.ILLEGAL_SCHEMA_ID, 'Illegal schema id, please check your schema info'); } const taskInfo = await this.requestTaskInfo(config.task_rpc, config.token, schemaId, chainType); const callbackUrl = config.callbackUrl || constants_1.DefaultCallbackUrl; const appBasePath = 'https://app.zkpass.org/verify'; let query = `app_id=${this.appid}&task_id=${taskInfo.task}&schema_id=${schemaId}&chain_type=${chainType}&callback_url=${callbackUrl}`; if (address) { query = `${query}/&account=${address}`; } if (device === 'Android') { (0, helper_1.launchAppForAndroid)(`zkpass://zkpass.com/verify?${query}`, `${appBasePath}?${query}`); return await this.getProofInfo(taskInfo.task, callbackUrl); } else if (device === 'iOS') { (0, helper_1.removeMetaTag)('apple-itunes-app'); (0, helper_1.injectMetaTag)('apple-itunes-app', 'app-clip-bundle-id=com.zkpass.transgate.clip, app-id=6738957441 app-clip-display=card'); const clipUrl = `https://appclip.apple.com/id?p=com.zkpass.transgate.clip&${query}`; this.handleIOSApp(clipUrl); return await this.getProofInfo(taskInfo.task, callbackUrl); } else if (this.transgateAvailable) { //support mobile but transgate is available and not mobile return await this.runTransgateExtension({ schemaId, address, taskInfo, chainType }); } else { //support mobile but transgate is not available generate a qrcode const launchUrl = `${appBasePath}?${query}`; return await this.runWithTransgateApp(launchUrl, taskInfo.task, callbackUrl); } } async runWithTransgateApp(launchUrl, taskId, callbackUrl) { try { const { canvasElement, remove } = (0, helper_1.insertQrcodeMask)(); await qrcode_1.default.toCanvas(canvasElement, launchUrl, { width: 240, }); const closeBtn = document.getElementById('close-transgate'); const zkpassCanvas = document.getElementById('zkpass-canvas'); closeBtn?.addEventListener('click', () => { remove(); this.terminal = true; }); this.getScanResult(taskId).then((taskUsed) => { if (taskUsed) { //@ts-ignore const ctx = zkpassCanvas.getContext('2d'); ctx.filter = 'blur(5px)'; ctx.drawImage(zkpassCanvas, 0, 0); } }); const proof = await this.getProofInfo(taskId, callbackUrl); if (proof) { remove(); } return proof; } catch (error) { if (this.terminal) { throw new error_1.TransgateError(error_1.ErrorCode.VERIFICATION_CANCELED, 'User terminal the validation.'); } throw new error_1.TransgateError(error_1.ErrorCode.UNEXPECTED_ERROR, error); } } async runTransgateExtension({ schemaId, address, taskInfo, chainType = 'evm', }) { const schemaUrl = `${this.baseServer}/schema/${schemaId}`; const schemaInfo = await this.requestSchemaInfo(schemaUrl); console.log('runTransgateExtension address', address); const { task, alloc_address: allocatorAddress, alloc_signature: signature, node_address: nodeAddress, node_host: nodeHost, node_pk: nodePK, } = taskInfo; const extensionParams = { task, allocatorAddress, nodeAddress, nodeHost, nodePK, signature, ...schemaInfo, appid: this.appid, }; if (!this.checkTaskInfo(chainType, task, schemaId, nodeAddress, signature)) { return new error_1.TransgateError(error_1.ErrorCode.ILLEGAL_TASK_INFO, 'Please ensure you connected the legitimate task nodes'); } this.launchTransgate(extensionParams, address); return new Promise((resolve, reject) => { const eventListener = (event) => { if (event.data.id !== extensionParams.id) { return; } if (event.data.type === types_1.EventDataType.INVALID_SCHEMA) { reject(new error_1.TransgateError(error_1.ErrorCode.ILLEGAL_SCHEMA, 'Incorrect schema information.')); } else if (event.data.type === types_1.EventDataType.GENERATE_ZKP_SUCCESS) { window?.removeEventListener('message', eventListener); const message = event.data; const { publicFields = [] } = message; const publicData = (0, helper_1.getObjectValues)(publicFields.map((item) => { delete item.str; return item; })); console.log('publicData', publicData); console.log('address', address); const proofResult = this.buildResult(message, taskInfo, publicData, allocatorAddress, address); console.log('proofResult', JSON.stringify(proofResult)); if (this.verifyProofMessageSignature(chainType, schemaId, proofResult)) { console.log('proofResult', proofResult); resolve(proofResult); } else { reject(new error_1.TransgateError(error_1.ErrorCode.ILLEGAL_NODE, 'The verification node is not the same as the node assigned to the task.')); } } else if (event.data.type === types_1.EventDataType.NOT_MATCH_REQUIREMENTS) { window?.removeEventListener('message', eventListener); reject(new error_1.TransgateError(error_1.ErrorCode.NOT_MATCH_REQUIREMENTS, 'The user does not meet the requirements.')); } else if (event.data.type === types_1.EventDataType.ILLEGAL_WINDOW_CLOSING) { window?.removeEventListener('message', eventListener); reject(new error_1.TransgateError(error_1.ErrorCode.VERIFICATION_CANCELED, 'The user closes the window before finishing validation.')); } else if (event.data.type === types_1.EventDataType.UNEXPECTED_VERIFY_ERROR) { window?.removeEventListener('message', eventListener); reject(new error_1.TransgateError(error_1.ErrorCode.UNEXPECTED_VERIFY_ERROR, 'An unexpected error was encountered, please try again.')); } }; window?.addEventListener('message', eventListener); }); } launchTransgate(taskInfo, address) { window?.postMessage({ type: 'AUTH_ZKPASS', mintAccount: address, ...taskInfo, }, '*'); } /** * request task info * @param {*} schemaId string schema id * @returns */ async requestTaskInfo(taskUrl, token, schemaId, chainType) { const response = await fetch(`https://${taskUrl}`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ token, schema_id: schemaId, app_id: this.appid, chain_type: chainType, debug: false, }), }); if (response.ok) { const result = await response.json(); return result.info; } throw new error_1.TransgateError(error_1.ErrorCode.TASK_RPC_ERROR, 'Request task info error'); } async requestConfig() { const response = await fetch(`${this.baseServer}/sdk/config`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ app_id: this.appid, }), }); if (response.ok) { const result = await response.json(); return result.info; } throw new error_1.TransgateError(error_1.ErrorCode.ILLEGAL_APPID, 'Please check your appid'); } /** * request schema detail info * @param schemaUrl */ async requestSchemaInfo(schemaUrl) { const response = await fetch(schemaUrl); if (response.ok) { return await response.json(); } throw new error_1.TransgateError(error_1.ErrorCode.ILLEGAL_SCHEMA_ID, 'Illegal schema url, please contact develop team!'); } async getProofInfo(taskId, callbackUrl) { return new Promise((resolve, reject) => { let loopCount = 0; const requestInfo = () => { loopCount++; if (loopCount > 300) { this.removeModal && this.removeModal(); reject(new error_1.TransgateError(error_1.ErrorCode.REQUEST_TIMEOUT, 'Request timeout, please try again')); return; } if (this.terminal) { this.removeModal && this.removeModal(); reject(new error_1.TransgateError(error_1.ErrorCode.VERIFICATION_CANCELED, 'User terminal the validation.')); return; } setTimeout(async () => { try { const response = await fetch(`${callbackUrl}?task_index=${taskId}`, { signal: AbortSignal.timeout(5000) }); if (response.ok) { const res = await response.json(); this.removeModal && this.removeModal(); resolve(res.info); } else { requestInfo(); } } catch (error) { requestInfo(); } }, 2000); }; requestInfo(); }); } async getScanResult(taskId) { return new Promise((resolve, reject) => { let loopCount = 0; const requestScanResult = () => { loopCount++; if (loopCount > 300) { reject(new error_1.TransgateError(error_1.ErrorCode.REQUEST_TIMEOUT, 'Request timeout, please try again')); return; } if (this.terminal) { reject(new error_1.TransgateError(error_1.ErrorCode.VERIFICATION_CANCELED, 'User terminal the validation.')); return; } setTimeout(async () => { try { const response = await await fetch(constants_1.ScanResultUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ task_id: taskId, }), }); if (response.ok) { const res = await response.json(); //Task ID has been used if (res.info.used) { resolve(true); } else { requestScanResult(); } } else { requestScanResult(); } } catch (error) { requestScanResult(); } }, 1000); }; requestScanResult(); }); } handleIOSModal() { const { remove } = (0, helper_1.insertMobileDialog)(); this.removeModal = remove; const closeBtn = document.getElementById('close-transgate'); closeBtn?.addEventListener('click', () => { remove(); this.terminal = true; }); } handleIOSApp(clipUrl) { const loading_box = document.getElementById('loading-box'); loading_box?.remove(); const complete_box = document.getElementById('complete-box'); const verify_button = document.getElementById('verify-button'); if (complete_box) { complete_box.style.display = 'flex'; verify_button?.addEventListener('click', () => { (0, helper_1.launchApp)(clipUrl); }); } } checkTaskInfo(chainType, task, schema, validatorAddress, signature) { if (chainType === 'sol') { return this.checkTaskInfoForSolana(task, schema, validatorAddress, signature); } else if (chainType === 'ton') { return this.checkTaskInfoForTon(task, schema, validatorAddress, signature); } const taskHex = web3_1.default.utils.stringToHex(task); const schemaHex = web3_1.default.utils.stringToHex(schema); return this.checkTaskInfoForEVM(taskHex, schemaHex, validatorAddress, signature); } checkTaskInfoForSolana(task, schema, validatorAddress, signature) { const sig_bytes = (0, helper_1.hexToBytes)(signature.slice(2)); const signatureBytes = sig_bytes.slice(0, 64); const recoverId = Array.from(sig_bytes.slice(64))[0]; const plaintext = borsh.serialize(solanaInstruction_1.SolanaTask, { task: task, schema: schema, notary: validatorAddress, }); const plaintextHash = buffer_1.Buffer.from(js_sha3_1.default.keccak_256.digest(buffer_1.Buffer.from(plaintext))); const address = secp256k1_1.default.ecdsaRecover(signatureBytes, recoverId, plaintextHash, false); return constants_1.SolanaTaskAllocator === js_sha3_1.default.keccak_256.hex(address.slice(1)); } checkTaskInfoForTon(task, schema, validatorAddress, signature) { const taskCell = (0, ton_1.beginCell)() .storeBuffer(buffer_1.Buffer.from(task, 'ascii')) .storeBuffer(buffer_1.Buffer.from(schema, 'ascii')) .storeBuffer(buffer_1.Buffer.from(validatorAddress, 'hex')) .endCell(); const taskVerify = (0, crypto_1.signVerify)(taskCell.hash(), buffer_1.Buffer.from(signature, 'hex'), buffer_1.Buffer.from(constants_1.TonTaskPubKey, 'hex')); return taskVerify; } checkTaskInfoForEVM(task, schema, validatorAddress, signature) { const web3 = new web3_1.default(); const encodeParams = web3.eth.abi.encodeParameters(['bytes32', 'bytes32', 'address'], [task, schema, validatorAddress]); const paramsHash = web3_1.default.utils.soliditySha3(encodeParams); const signedAllocatorAddress = web3.eth.accounts.recover(paramsHash, signature); return constants_1.EVMTaskAllocator === signedAllocatorAddress; } /** * check the proof result by chain type * @param chainType * @param schema * @param proofResult * @returns */ verifyProofMessageSignature(chainType, schema, proofResult) { const { taskId, publicFieldsHash, uHash, validatorAddress, validatorSignature, recipient } = proofResult; const taskHex = web3_1.default.utils.stringToHex(taskId); const schemaHex = web3_1.default.utils.stringToHex(schema); if (chainType === 'sol') { const rec = recipient; return this.verifyMessageSignatureForSolana({ taskId, uHash, validatorAddress, schema, validatorSignature, recipient: rec, publicFieldsHash, }); } else if (chainType === 'ton') { const rec = recipient; return this.verifyMessageSignatureForTon({ taskId, uHash, validatorAddress, schema, validatorSignature, recipient: rec, publicFieldsHash, }); } return this.verifyEVMMessageSignature(taskHex, schemaHex, uHash, publicFieldsHash, validatorSignature, validatorAddress, recipient); } verifyEVMMessageSignature(taskId, schema, nullifier, publicFieldsHash, signature, originAddress, recipient) { const web3 = new web3_1.default(); const types = ['bytes32', 'bytes32', 'bytes32', 'bytes32']; const values = [taskId, schema, nullifier, publicFieldsHash]; if (recipient) { types.push('address'); values.push(recipient); } const encodeParams = web3.eth.abi.encodeParameters(types, values); const paramsHash = web3_1.default.utils.soliditySha3(encodeParams); const nodeAddress = web3.eth.accounts.recover(paramsHash, signature); return nodeAddress === originAddress; } /** * check signature is matched with task info * @param params * @returns */ verifyMessageSignatureForSolana(params) { const { taskId, uHash, validatorAddress, schema, validatorSignature, recipient, publicFieldsHash } = params; const sig_bytes = (0, helper_1.hexToBytes)(validatorSignature.slice(2)); const signatureBytes = sig_bytes.slice(0, 64); const recoverId = Array.from(sig_bytes.slice(64))[0]; const plaintext = borsh.serialize(solanaInstruction_1.Attest, { task: taskId, nullifier: uHash, schema, recipient, publicFieldsHash, }); const plaintextHash = buffer_1.Buffer.from(js_sha3_1.default.keccak_256.digest(buffer_1.Buffer.from(plaintext))); const address = secp256k1_1.default.ecdsaRecover(signatureBytes, recoverId, plaintextHash, false); return validatorAddress === js_sha3_1.default.keccak_256.hex(address.slice(1)); } buildResult(data, taskInfo, publicData, allocatorAddress, recipient) { const { publicFields, taskId, nullifierHash, signature } = data; const { node_address: nodeAddress, alloc_signature: allocSignature } = taskInfo; const publicFieldsHash = web3_1.default.utils.soliditySha3(!!publicData ? web3_1.default.utils.stringToHex(publicData) : web3_1.default.utils.utf8ToHex('1')); return { taskId, publicFields, allocatorAddress, publicFieldsHash, allocatorSignature: allocSignature, uHash: nullifierHash, validatorAddress: nodeAddress, validatorSignature: signature, recipient, }; } verifyMessageSignatureForTon(params) { const { taskId, uHash, validatorAddress, schema, validatorSignature, recipient, publicFieldsHash } = params; const attestationCell = (0, ton_1.beginCell)() .storeRef((0, ton_1.beginCell)() .storeBuffer(buffer_1.Buffer.from(taskId, 'ascii')) .storeBuffer(buffer_1.Buffer.from(schema, 'ascii')) .storeBuffer(buffer_1.Buffer.from(uHash.slice(2), 'hex')) .endCell()) .storeAddress(ton_1.Address.parse(recipient)) .storeRef((0, ton_1.beginCell)() .storeBuffer(buffer_1.Buffer.from(publicFieldsHash.slice(2), 'hex')) .endCell()) .endCell(); const attestationVerify = (0, crypto_1.signVerify)(attestationCell.hash(), buffer_1.Buffer.from(validatorSignature.slice(2), 'hex'), buffer_1.Buffer.from(validatorAddress, 'hex')); return attestationVerify; } async isTransgateAvailable() { try { const url = `chrome-extension://${constants_1.extensionId}/images/icon-16.png`; const { statusText } = await fetch(url); if (statusText === 'OK') { this.transgateAvailable = true; return true; } return false; } catch (error) { return false; } } } exports.default = TransgateConnect;