UNPKG

@nori-zk/mina-token-bridge

Version:

Nori ethereum state settelment and nETH token bridge zkApp

214 lines 15.1 kB
import 'dotenv/config'; import { Logger, LogPrinter } from 'esm-iso-logger'; 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 { getTokenBridgeWorker } from '../../workers/tokenBridgeWorker/node/parent.js'; import { ethers } from 'ethers'; import { noriTokenBridgeJson as noriEthTokenBridgeJson } from '@nori-zk/ethereum-token-bridge'; import { getStagingEnv, validateEnv } from '../testUtils.js'; import { createTimer } from '@nori-zk/o1js-zk-utils'; // https://faucet.minaprotocol.com/ new LogPrinter('TestTokenBridge'); const logger = new Logger('E2EDevnetSpec'); const { NORI_ETH_TOKEN_BRIDGE_ADDRESS: noriETHBridgeAddressHex, NORI_MINA_TOKEN_BRIDGE_ADDRESS: noriTokenBridgeAddressBase58, NORI_MINA_TOKEN_BASE_ADDRESS: noriTokenBaseAddressBase58, MINA_RPC_NETWORK_URL: minaRpcUrl, MINA_ARCHIVE_RPC_URL: minaArchiveRpcUrl, MINA_RPC_NETWORK_ID: minaRpcNetworkId, NORI_WSS_URL: noriWssUrl, NORI_PCS_URL: noriPcsUrl, } = getStagingEnv(); describe('e2e_testnet', () => { const minaConfig = { networkId: minaRpcNetworkId, mina: minaRpcUrl, archive: minaArchiveRpcUrl, }; test('e2e_complete_testnet', async () => { let depositProcessingStatusSubscription; try { // Get ENV VARS const { ethPrivateKey, ethRpcUrl, minaSenderPrivateKeyBase58, } = validateEnv(); const minaSenderPrivateKey = PrivateKey.fromBase58(minaSenderPrivateKeyBase58); const minaSenderPublicKey = minaSenderPrivateKey.toPublicKey(); const minaSenderPublicKeyBase58 = minaSenderPublicKey.toBase58(); // GET ETH WALLET ************************************************** logger.log('Getting ETH wallet.'); const etherProvider = new ethers.JsonRpcProvider(ethRpcUrl); const ethWallet = new ethers.Wallet(ethPrivateKey, etherProvider); // START MAIN FLOW // INIT WORKER ************************************************** logger.log('Fetching zkApp worker.'); const TokenBridgeWorker = getTokenBridgeWorker(); const tokenBridgeWorker = new TokenBridgeWorker(); // Configure wallet // In reality we would not pass this from the main thread. We would rely on the WALLET for signatures. await tokenBridgeWorker.WALLET_setMinaPrivateKey(minaSenderPrivateKeyBase58); await tokenBridgeWorker.minaSetup(minaConfig); // OBTAIN CREDENTIAL ************************************************** // CLIENT ******************* // Note this value is used to restrict the domain of the signature but could // also be a user provided secret for extra security. const messageSCRAMStr = 'NoriZK25'; // Sign SCRAM message using Mina private key (via worker). // This is used such that we can deterministically derive our codeChallenge // used for the SCRAM code exchange without the user having to store // any secret, when a fixed message is used. // If the user uses a fixed message then they could use their Mina key to re generate // their signature (and therefore codeChallenge) on another machine. // If they provided a secret message then they would have to keep this themselves and provide it when minting. logger.log('Signing SCRAM message'); const scramSignTimer = createTimer(); const signatureSCRAMBase58 = await tokenBridgeWorker.MOCK_SCRAM_signMessage(messageSCRAMStr); logger.log(`SCRAM signature computed in ${scramSignTimer()}`); // CLIENT only logic from now on.... // Create code challenge from SCRAM signature const codeChallengeSCRAMStr = await tokenBridgeWorker.SCRAM_createCodeChallenge(signatureSCRAMBase58); const codeChallengeSCRAMBigInt = BigInt(codeChallengeSCRAMStr); // These prints are just for testing purposes. logger.log('senderPublicKey.toBase58()', minaSenderPublicKeyBase58); logger.log('senderPrivateKey.toBase58()', minaSenderPrivateKeyBase58); logger.log('signatureSCRAMBase58', signatureSCRAMBase58); logger.log('codeChallengeSCRAMBigInt', codeChallengeSCRAMBigInt); logger.log('codeChallengeSCRAMStr', codeChallengeSCRAMStr); // CONNECT TO BRIDGE ************************************************** // Establish a connection to the bridge. logger.log('Establishing bridge connection and topics.'); const { bridgeSocket$, bridgeSocketConnectionState$ } = getReconnectingBridgeSocket$(noriWssUrl); // Subscribe to the sockets connection status. bridgeSocketConnectionState$.subscribe({ next: (state) => logger.log(`[WS] ${state}`), error: (state) => logger.error(`[WS] ${state}`), complete: () => logger.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. logger.log('Awaiting sufficient bridge state'); const bridgeStateReadyTimer = createTimer(); await bridgeStatusesKnownEnoughToLockUnsafe(ethStateTopic$, bridgeStateTopic$, bridgeTimingsTopic$); logger.log(`Bridge state ready in ${bridgeStateReadyTimer()}`); // LOCK TOKENS ************************************************** logger.log('Locking eth tokens'); const lockingTokensTimer = createTimer(); const abi = noriEthTokenBridgeJson.abi; const contract = new ethers.Contract(noriETHBridgeAddressHex, abi, ethWallet); const credentialAttestationBigNumberIsh = codeChallengeSCRAMBigInt; const depositAmountStr = '0.0001'; // 100 BU (minimum lock amount) logger.log('depositAmountStr', depositAmountStr); const depositAmount = ethers.parseEther(depositAmountStr); const result = await contract.lockTokens(credentialAttestationBigNumberIsh, { value: depositAmount }); logger.log('Eth deposit made', result); logger.log('Waiting for 1 confirmation'); const confirmedResult = await result.wait(); logger.log('Confirmed Eth Deposit', confirmedResult); const depositBlockNumber = confirmedResult.blockNumber; if (!depositBlockNumber) { logger.error('depositBlockNumber was falsey'); } logger.log(`Deposit confirmed with blockNumber: ${depositBlockNumber}`); logger.log(`Tokens locked in ${lockingTokensTimer()}`); // 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: (msg) => logger.info(msg), error: (err) => logger.error(err), complete: () => logger.warn('Deposit processing completed. Mint opportunity has been missed :('), }); // COMPUTE DEPOSIT ATTESTATION ************************************************** // PREPARE FOR MINTING ************************************************** // Compile tokenBridgeWorker dependancies logger.log('Compiling dependancies of tokenBridgeWorker'); const tokenBridgeWorkerReady = tokenBridgeWorker.compileAll(); // ?? Can we move this earlier... // Get noriStorageInterfaceVerificationKeySafe from tokenBridgeWorkerReady resolution. const { noriStorageInterfaceVerificationKeySafe } = await tokenBridgeWorkerReady; logger.log('Awaited compilation of tokenBridgeWorkerReady'); // SETUP STORAGE ************************************************** // TODO IMPROVE THIS const setupRequired = await tokenBridgeWorker.needsToSetupStorage(noriTokenBridgeAddressBase58, minaSenderPublicKeyBase58); logger.log(`Setup storage required? '${setupRequired}'`); if (setupRequired) { logger.log('Setting up storage'); const setupStorageTimer = createTimer(); const { txHash: setupTxHash } = await tokenBridgeWorker.MOCK_setupStorage(minaSenderPublicKeyBase58, noriTokenBridgeAddressBase58, 0.1 * 1e9, noriStorageInterfaceVerificationKeySafe); // NOTE! ************ // Really a client would use await tokenBridgeWorker.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 tokenBridgeWorker.setupStorage should be used on the // frontend. /*const provedSetupTxStr = await tokenBridgeWorker.setupStorage( senderPublicKeyBase58, noriTokenBridgeAddressBase58, 0.1 * 1e9, noriStorageInterfaceVerificationKeySafe ); logger.log('provedSetupTxStr', provedSetupTxStr);*/ // The below should use a real wallets signAndSend method. /*const { txHash: setupTxHash } = await tokenBridgeWorker.WALLET_signAndSend(provedSetupTxStr);*/ logger.log('setupTxHash', setupTxHash); logger.log(`Nori minter storage setup in ${setupStorageTimer()}`); } // Block until we can compute our deposit attestation proof. logger.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$); // Compute deposit witness logger.log('Computing deposit witness.'); const depositAttestationInput = await tokenBridgeWorker.computeDepositAttestationWitness(codeChallengeSCRAMStr, depositBlockNumber, noriPcsUrl); logger.log('Computed deposit witness.'); // PRE-COMPUTE MINT PROOF **************************************************** logger.log('Determining user funding status.'); const needsToFundAccount = await tokenBridgeWorker.needsToFundAccount(noriTokenBaseAddressBase58, minaSenderPublicKeyBase58); logger.log('needsToFundAccount', needsToFundAccount); // WAIT FOR DEPOSIT PROCESSING COMPLETED BY BRIDGE BEFORE SENDING OUR MINT PROOF TO MINA ********************** logger.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$); logger.log('Deposit is processed signing and sending the mint proof.'); logger.log('Computing mint proof.'); const mintProofComputationTimer = createTimer(); await tokenBridgeWorker.MOCK_computeMintProofAndCache(minaSenderPublicKeyBase58, noriTokenBridgeAddressBase58, depositAttestationInput, messageSCRAMStr, signatureSCRAMBase58, 1e9 * 0.1, needsToFundAccount); logger.log(`Mint proof computation in ${mintProofComputationTimer()}`); // NOTE! // Really a client would use await tokenBridgeWorker.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 tokenBridgeWorker.mint should be used on the // frontend, and at this stage, instead of the above: /*const provedMintTxStr = await tokenBridgeWorker.mint( senderPublicKeyBase58, noriTokenBridgeAddressBase58, // CHECKME @Karol { ethDepositProofJson: ethDepositProofJson, presentationProofStr: presentationJsonStr, }, 1e9 * 0.1, true ); logger.log('provedMintTxStr', provedMintTxStr);*/ // SIGN AND SEND MINT PROOF ************************************************** const mintTransactionFinalizedTimer = createTimer(); const { txHash: mintTxHash } = await tokenBridgeWorker.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 tokenBridgeWorker.WALLET_signAndSend(provedMintTxStr);*/ logger.log('mintTxHash', mintTxHash); logger.log(`Mint transaction finalized in ${mintTransactionFinalizedTimer()}`); logger.log('Minted!'); // Get the amount minted so far and print it const mintedSoFar = await tokenBridgeWorker.mintedSoFar(noriTokenBridgeAddressBase58, minaSenderPublicKeyBase58); logger.log('mintedSoFar', mintedSoFar); const balanceOfUser = await tokenBridgeWorker.getBalanceOf(noriTokenBaseAddressBase58, minaSenderPublicKeyBase58); logger.log('balanceOfUser', balanceOfUser); // END MAIN FLOW } finally { if (depositProcessingStatusSubscription) depositProcessingStatusSubscription.unsubscribe(); } }, 1000000000); }); //# sourceMappingURL=nori-only.devnet.e2e.spec.js.map