@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
JavaScript
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;
;