@nori-zk/mina-token-bridge
Version:
A Mina zk-program contract allowing users to mint tokens on Nori Bridge.
273 lines • 17.5 kB
JavaScript
import 'dotenv/config';
import { PrivateKey } from 'o1js';
import { getReconnectingBridgeSocket$ } from './rx/socket.js';
import { getBridgeStateTopic$, getBridgeTimingsTopic$, getEthStateTopic$, } from './rx/topics.js';
import { bridgeStatusesKnownEnoughToLockUnsafe, canMint, getDepositProcessingStatus$, readyToComputeMintProof, } from './rx/deposit.js';
import { signSecretWithEthWallet } from './ethSignature.js';
import { getTokenMintWorker } from './workers/tokenMint/node/parent.js';
import { getCredentialAttestationWorker } from './workers/credentialAttestation/node/parent.js';
import { ethers } from 'ethers';
import { noriTokenBridgeJson } from '@nori-zk/ethereum-token-bridge';
function validateEnv() {
const errors = [];
const { ETH_PRIVATE_KEY, ETH_RPC_URL, NORI_TOKEN_BRIDGE_ADDRESS, NORI_CONTROLLER_PUBLIC_KEY, MINA_RPC_NETWORK_URL, SENDER_PRIVATE_KEY, NORI_TOKEN_PUBLIC_KEY, } = process.env;
if (!ETH_PRIVATE_KEY || !/^[a-fA-F0-9]{64}$/.test(ETH_PRIVATE_KEY)) {
errors.push('ETH_PRIVATE_KEY missing or invalid (expected 64 hex chars, no 0x prefix)');
}
if (!ETH_RPC_URL || !/^https?:\/\//.test(ETH_RPC_URL)) {
errors.push('ETH_RPC_URL missing or invalid (expected http(s) URL)');
}
if (!NORI_TOKEN_BRIDGE_ADDRESS ||
!/^0x[a-fA-F0-9]{40}$/.test(NORI_TOKEN_BRIDGE_ADDRESS)) {
errors.push('NORI_TOKEN_BRIDGE_ADDRESS missing or invalid (expected 0x-prefixed 40 hex chars)');
}
if (!NORI_CONTROLLER_PUBLIC_KEY ||
!/^[1-9A-HJ-NP-Za-km-z]+$/.test(NORI_CONTROLLER_PUBLIC_KEY)) {
errors.push('NORI_CONTROLLER_PUBLIC_KEY missing or invalid (expected Base58 string)');
}
if (!NORI_TOKEN_PUBLIC_KEY ||
!/^[1-9A-HJ-NP-Za-km-z]+$/.test(NORI_TOKEN_PUBLIC_KEY)) {
errors.push('NORI_TOKEN_PUBLIC_KEY missing or invalid (expected Base58 string)');
}
if (!MINA_RPC_NETWORK_URL || !/^https?:\/\//.test(MINA_RPC_NETWORK_URL)) {
errors.push('MINA_RPC_NETWORK_URL missing or invalid (expected http(s) URL)');
}
if (!SENDER_PRIVATE_KEY ||
!/^[1-9A-HJ-NP-Za-km-z]+$/.test(SENDER_PRIVATE_KEY)) {
errors.push('SENDER_PRIVATE_KEY missing or invalid (expected Base58 string)');
}
if (errors.length) {
console.error('Environment validation errors:');
errors.forEach((e) => console.error(' - ' + e));
process.exit(1);
}
return {
ethPrivateKey: ETH_PRIVATE_KEY,
ethRpcUrl: ETH_RPC_URL,
noriETHBridgeAddressHex: NORI_TOKEN_BRIDGE_ADDRESS,
noriTokenControllerAddressBase58: NORI_CONTROLLER_PUBLIC_KEY,
noriTokenBaseAddressBase58: NORI_TOKEN_PUBLIC_KEY,
minaRpcUrl: MINA_RPC_NETWORK_URL,
minaSenderPrivateKeyBase58: SENDER_PRIVATE_KEY,
};
}
// https://faucet.minaprotocol.com/
describe('e2e_testnet', () => {
test('e2e_complete_testnet', async () => {
let depositProcessingStatusSubscription;
try {
// Get ENV VARS
const { ethPrivateKey, ethRpcUrl, noriETHBridgeAddressHex, noriTokenControllerAddressBase58, minaRpcUrl, minaSenderPrivateKeyBase58, noriTokenBaseAddressBase58, } = validateEnv();
const minaSenderPrivateKey = PrivateKey.fromBase58(minaSenderPrivateKeyBase58);
const minaSenderPublicKey = minaSenderPrivateKey.toPublicKey();
const minaSenderPublicKeyBase58 = minaSenderPublicKey.toBase58();
// Define litenet mina config
const minaConfig = {
networkId: 'testnet',
mina: minaRpcUrl,
};
// GET ETH WALLET **************************************************
console.log('Getting ETH wallet.');
const etherProvider = new ethers.JsonRpcProvider(ethRpcUrl);
const ethWallet = new ethers.Wallet(ethPrivateKey, etherProvider);
const ethAddressLowerHex = ethWallet.address.toLowerCase();
// INIT WORKERS **************************************************
console.log('Fetching workers.');
const tokenMintWorker = getTokenMintWorker();
const credentialAttestationWorker = getCredentialAttestationWorker();
// READY CREDENTIAL ATTESTATION WORKER **************************************
console.log('Compiling credentialAttestationWorker dependancies.');
const credentialAttestationReady = credentialAttestationWorker.compile();
// START MAIN FLOW
// OBTAIN CREDENTIAL **************************************************
// CLIENT *******************
const secret = 'IAmASecretOfLength20';
// Get signature
console.log('Creating eth signature of our secret');
console.time('ethSecretSignature');
const ethSecretSignature = await signSecretWithEthWallet(secret, ethWallet);
console.timeEnd('ethSecretSignature');
// These prints are just for testing purposes.
console.log('ethSecretSignature', ethSecretSignature);
console.log('senderPublicKey.toBase58()', minaSenderPublicKeyBase58);
// CLIENT *******************
console.log('Awaiting credentialAttestation compile.');
await credentialAttestationReady;
// Create credential
console.log('Creating credential');
console.time('createCredential');
// This would be sent from the CLIENT to the WALLET to store.
const credentialJson = await credentialAttestationWorker.computeCredential(secret, ethSecretSignature, ethWallet.address, minaSenderPublicKeyBase58);
console.timeEnd('createCredential'); // 2:02.513 (m:ss.mmm)
// CLIENT *******************
// Create a presentation request
// This is sent from the CLIENT to the WALLET
console.log('Creating presentation request');
console.time('getPresentationRequest');
const presentationRequestJson = await credentialAttestationWorker.computeEcdsaSigPresentationRequest(noriTokenControllerAddressBase58);
console.timeEnd('getPresentationRequest'); // 1.348ms
// WALLET ********************
// WALLET takes a presentation request and the WALLET can retrieve the stored credential
// From this it creates a presentation and sends this to the CLIENT
console.log('Creating presentation');
console.time('getPresentation');
const presentationJsonStr = await credentialAttestationWorker.WALLET_computeEcdsaSigPresentation(presentationRequestJson, credentialJson, minaSenderPrivateKeyBase58);
console.timeEnd('getPresentation'); // 46.801s
// Kill credentialAttestation worker to reclaim ram.
credentialAttestationWorker.terminate();
console.log('credentialAttestationWorker terminated');
// CLIENT only logic from now on....
// Extract hashed secret from presentation
const presentation = JSON.parse(presentationJsonStr);
const messageHashString = presentation.outputClaim.value.messageHash.value;
const credentialAttestationBigInt = BigInt(messageHashString);
// CONNECT TO BRIDGE **************************************************
// Establish a connection to the bridge.
console.log('Establishing bridge connection and topics.');
const { bridgeSocket$, bridgeSocketConnectionState$ } = getReconnectingBridgeSocket$();
// Subscribe to the sockets connection status.
bridgeSocketConnectionState$.subscribe({
next: (state) => console.log(`[WS] ${state}`),
error: (state) => console.error(`[WS] ${state}`),
complete: () => console.log('[WS] Bridge socket connection completed.'),
});
// Retrieve observables for the bridge topics needed.
const ethStateTopic$ = getEthStateTopic$(bridgeSocket$);
const bridgeStateTopic$ = getBridgeStateTopic$(bridgeSocket$);
const bridgeTimingsTopic$ = getBridgeTimingsTopic$(bridgeSocket$);
// Wait for bridge topics to be ready, to ensure correct deposit classification.
// Under normal conditions this is very fast. But see the docstring for why this
// may be unsafe, a safe method is also provided.
console.log('Awaiting sufficient bridge state');
console.time('bridgeStateReady');
await bridgeStatusesKnownEnoughToLockUnsafe(ethStateTopic$, bridgeStateTopic$, bridgeTimingsTopic$);
console.timeEnd('bridgeStateReady');
// LOCK TOKENS **************************************************
console.log('Locking eth tokens');
console.time('lockingTokens');
const abi = noriTokenBridgeJson.abi;
const contract = new ethers.Contract(noriETHBridgeAddressHex, abi, ethWallet);
const credentialAttestationBigNumberIsh = credentialAttestationBigInt;
const depositAmountStr = '0.000001';
console.log('depositAmountStr', depositAmountStr);
const depositAmount = ethers.parseEther(depositAmountStr);
const result = await contract.lockTokens(credentialAttestationBigNumberIsh, { value: depositAmount });
console.log('Eth deposit made', result);
console.log('Waiting for 1 confirmation');
const confirmedResult = await result.wait();
console.log('Confirmed Eth Deposit', confirmedResult);
const depositBlockNumber = confirmedResult.blockNumber;
if (!depositBlockNumber) {
console.error('depositBlockNumber was falsey');
}
console.log(`Deposit confirmed with blockNumber: ${depositBlockNumber}`);
console.timeEnd('lockingTokens');
// ESTABLISH DEPOSIT BRIDGE PROCESSING STATUS **********************************
// Get deposit status given our execution block number from the tx receipt.
const depositProcessingStatus$ = getDepositProcessingStatus$(depositBlockNumber, ethStateTopic$, bridgeStateTopic$, bridgeTimingsTopic$);
// Subscribe to the depositProcessingStatus observable to print our progress.
depositProcessingStatusSubscription =
depositProcessingStatus$.subscribe({
next: console.log,
error: console.error,
complete: () => console.warn('Deposit processing completed. Mint opportunity has been missed :('),
});
// COMPUTE DEPOSIT ATTESTATION **************************************************
// Compile tokenMintWorker dependancies
console.log('Compiling dependancies of tokenMintWorker');
const tokenMintWorkerReady = tokenMintWorker.compileAll(); // ?? Can we move this earlier...
// PREPARE FOR MINTING **************************************************
// Configure wallet
// In reality we would not pass this from the main thread. We would rely on the WALLET for signatures.
await tokenMintWorker.WALLET_setMinaPrivateKey(minaSenderPrivateKeyBase58);
await tokenMintWorker.minaSetup(minaConfig);
// Get noriTokenControllerVerificationKeySafe from tokenMintWorkerReady resolution.
const noriTokenControllerVerificationKeySafe = await tokenMintWorkerReady;
console.log('Awaited compilation of tokenMintWorkerReady');
// SETUP STORAGE **************************************************
// TODO IMPROVE THIS
const setupRequired = await tokenMintWorker.needsToSetupStorage(noriTokenControllerAddressBase58, minaSenderPublicKeyBase58);
console.log(`Setup storage required? '${setupRequired}'`);
if (setupRequired) {
console.log('Setting up storage');
console.time('noriMinter.setupStorage');
const { txHash: setupTxHash } = await tokenMintWorker.MOCK_setupStorage(minaSenderPublicKeyBase58, noriTokenControllerAddressBase58, 0.1 * 1e9, noriTokenControllerVerificationKeySafe);
// NOTE! ************
// Really a client would use await tokenMintWorker.setupStorage(...args) and get a provedSetupTxStr which would be submitted to the WALLET for signing
// Currently we don't have the correct logic for emulating the wallet signAndSend method. However tokenMintWorker.setupStorage should be used on the
// frontend.
/*const provedSetupTxStr = await tokenMintWorker.setupStorage(
senderPublicKeyBase58,
noriTokenControllerAddressBase58,
0.1 * 1e9,
noriTokenControllerVerificationKeySafe
);
console.log('provedSetupTxStr', provedSetupTxStr);*/
// The below should use a real wallets signAndSend method.
/*const { txHash: setupTxHash } =
await tokenMintWorker.WALLET_signAndSend(provedSetupTxStr);*/
console.log('setupTxHash', setupTxHash);
console.timeEnd('noriMinter.setupStorage');
}
// Block until we can compute our deposit attestation proof.
console.log('Waiting for ProofConversionJobSucceeded on WaitingForCurrentJobCompletion before we can compute our EthDeposit proof.');
// Waits for proof conversion to be finished.
// Throws if we have missed our minting opportunity.
await readyToComputeMintProof(depositProcessingStatus$);
console.log('Computing eth deposit proof.');
const { ethDepositProofJson } = await tokenMintWorker.computeEthDeposit(presentationJsonStr, depositBlockNumber, ethAddressLowerHex);
// PRE-COMPUTE MINT PROOF ****************************************************
console.log('Computing mint proof.');
console.time('Mint proof computation');
await tokenMintWorker.MOCK_computeMintProofAndCache(minaSenderPublicKeyBase58, noriTokenControllerAddressBase58, {
ethDepositProofJson: ethDepositProofJson,
presentationProofStr: presentationJsonStr,
}, 1e9 * 0.1, true);
console.timeEnd('Mint proof computation');
// NOTE!
// Really a client would use await tokenMintWorker.mint(...args) and get a provedMintTxStr which would be submitted to the WALLET for signing
// Currently we don't have the correct logic for emulating the wallet signAndSend method. However tokenMintWorker.mint should be used on the
// frontend, and at this stage, instead of the above:
/*const provedMintTxStr = await tokenMintWorker.mint(
senderPublicKeyBase58,
noriTokenControllerAddressBase58, // CHECKME @Karol
{
ethDepositProofJson: ethDepositProofJson,
presentationProofStr: presentationJsonStr,
},
1e9 * 0.1,
true
);
console.log('provedMintTxStr', provedMintTxStr);*/
// WAIT FOR DEPOSIT PROCESSING COMPLETED BY BRIDGE BEFORE SENDING OUR MINT PROOF TO MINA **********************
console.log('Waiting for deposit processing completion before we can sign and send the mint proof.');
// Block until deposit has been processed (when the depositProcessingStatus$ observable completes)
// Throws if we have missed our minting opportunity
await canMint(depositProcessingStatus$);
console.log('Deposit is processed signing and sending the mint proof.');
// SIGN AND SEND MINT PROOF **************************************************
console.time('Mint transaction finalized');
const { txHash: mintTxHash } = await tokenMintWorker.WALLET_MOCK_signAndSendMintProofCache();
// Note a client would really use a wallet.signAndSend(provedMintTxStr) method at this point instead of the above.
// And ideally when WALLET_signAndSend works properly we would replace the above(within this test only!) with the below MOCK for wallet behaviour.
/*const { txHash: mintTxHash } =
await tokenMintWorker.WALLET_signAndSend(provedMintTxStr);*/
console.log('mintTxHash', mintTxHash);
console.timeEnd('Mint transaction finalized');
console.log('Minted!');
// Get the amount minted so far and print it
const mintedSoFar = await tokenMintWorker.mintedSoFar(noriTokenControllerAddressBase58, minaSenderPublicKeyBase58);
console.log('mintedSoFar', mintedSoFar);
const balanceOfUser = await tokenMintWorker.getBalanceOf(noriTokenBaseAddressBase58, minaSenderPublicKeyBase58);
console.log('balanceOfUser', balanceOfUser);
// END MAIN FLOW
}
catch (e) {
throw e;
}
finally {
depositProcessingStatusSubscription.unsubscribe();
}
}, 1000000000);
});
//# sourceMappingURL=e2e.workers3.spec.js.map