@nori-zk/mina-token-bridge
Version:
A Mina zk-program contract allowing users to mint tokens on Nori Bridge.
226 lines • 15 kB
JavaScript
import { PrivateKey } from 'o1js';
import { getEthWallet, getNewMinaLiteNetAccountSK, lockTokens, } from './testUtils.js';
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 { getSecretHashFromPresentationJson } from './credentialAttestation.js';
import { getTokenMintWorker } from './workers/tokenMint/node/parent.js';
import { getTokenDeployerWorker } from './workers/tokenDeployer/node/parent.js';
import { getCredentialAttestationWorker } from './workers/credentialAttestation/node/parent.js';
describe('e2e', () => {
test('e2e_complete', async () => {
let depositProcessingStatusSubscription;
try {
// Define litenet mina config
const minaConfig = {
networkId: 'devnet',
mina: 'http://localhost:8080/graphql',
};
// 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();
// DEPLOY TEST CONTRACTS **************************************************
// Deploy token minter contracts (Note this will normally be done already for the user, this is just for testing)
// Use the worker to be able to reclaim some ram
console.log('Deploying contract.');
const tokenDeployer = getTokenDeployerWorker();
const storageInterfaceVerificationKeySafe = await tokenDeployer.compile();
const contractsLitenetSk = await getNewMinaLiteNetAccountSK();
const contractSenderPrivateKey = PrivateKey.fromBase58(contractsLitenetSk);
const contractSenderPrivateKeyBase58 = contractSenderPrivateKey.toBase58();
const tokenControllerPrivateKey = PrivateKey.random();
const tokenBasePrivateKey = PrivateKey.random();
const ethProcessorAddress = PrivateKey.random()
.toPublicKey()
.toBase58();
await tokenDeployer.minaSetup(minaConfig);
const { tokenBaseAddress: tokenBaseAddressBase58, noriTokenControllerAddress: noriTokenControllerAddressBase58, } = await tokenDeployer.deployContracts(contractSenderPrivateKeyBase58, contractSenderPrivateKeyBase58, // Admin
tokenControllerPrivateKey.toBase58(), tokenBasePrivateKey.toBase58(), ethProcessorAddress, storageInterfaceVerificationKeySafe, 0.1 * 1e9, {
symbol: 'nETH',
decimals: 18,
allowUpdates: true,
});
tokenDeployer.terminate();
// Generate a funded test private key for mina litenet
const litenetSk = await getNewMinaLiteNetAccountSK();
const senderPrivateKey = PrivateKey.fromBase58(litenetSk);
const senderPrivateKeyBase58 = senderPrivateKey.toBase58();
const senderPublicKey = senderPrivateKey.toPublicKey();
const senderPublicKeyBase58 = senderPublicKey.toBase58();
// START MAIN FLOW
// GET WALLET **************************************************
console.log('Getting ETH wallet.');
const ethWallet = await getEthWallet();
const ethAddressLowerHex = ethWallet.address.toLowerCase();
// 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('senderPrivateKey.toBase58()', senderPrivateKeyBase58);
console.log('senderPublicKey.toBase58()', senderPublicKeyBase58);
// 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, senderPublicKeyBase58);
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.time('getPresentation');
console.log('Creating presentation');
const presentationJsonStr = await credentialAttestationWorker.WALLET_computeEcdsaSigPresentation(presentationRequestJson, credentialJson, senderPrivateKeyBase58);
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 { credentialAttestationBEHex, credentialAttestationHashField, } = getSecretHashFromPresentationJson(presentationJsonStr);
console.log('attestationBEHex', credentialAttestationBEHex);
// 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 depositAmount = 0.000001;
console.log('Deposit amount', depositAmount);
const depositBlockNumber = await lockTokens(credentialAttestationHashField, depositAmount);
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();
// Block until we can compute our deposit attestation proof.
console.log('Waiting for ProofConversionJobSucceeded on WaitingForCurrentJobCompletion before we can compute our EthDeposit proof.');
// Throws if we have missed our minting opportunity
await readyToComputeMintProof(depositProcessingStatus$);
console.log('Computing eth deposit proof.');
const { ethDepositProofJson, despositSlotRaw } = await tokenMintWorker.computeEthDeposit(presentationJsonStr, depositBlockNumber, ethAddressLowerHex);
console.log(`bridge head [attestationHash] (BE hex):`, despositSlotRaw.slot_nested_key_attestation_hash);
// WAIT FOR DEPOSIT PROCESSING COMPLETED BY BRIDGE ***************************
console.log('Waiting for deposit processing completion before we can complete the minting process.');
// 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 unblocking mint process.');
// 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(senderPrivateKeyBase58);
await tokenMintWorker.minaSetup(minaConfig);
// Get noriTokenControllerVerificationKeySafe from tokenMintWorkerReady resolution.
const noriTokenControllerVerificationKeySafe = await tokenMintWorkerReady;
console.log('Awaited compilation of tokenMintWorkerReady');
// SETUP STORAGE **************************************************
console.time('noriMinter.setupStorage');
const { txHash: setupTxHash } = await tokenMintWorker.MOCK_setupStorage(senderPublicKeyBase58, 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);*/
// MOCK for wallet behaviour
/*const { txHash: setupTxHash } =
await tokenMintWorker.WALLET_signAndSend(provedSetupTxStr);*/
console.log('setupTxHash', setupTxHash);
console.timeEnd('noriMinter.setupStorage');
// MINT **************************************************
console.time('Minting');
const { txHash: mintTxHash } = await tokenMintWorker.MOCK_mint(senderPublicKeyBase58, noriTokenControllerAddressBase58, {
ethDepositProofJson: ethDepositProofJson,
presentationProofStr: presentationJsonStr,
}, 1e9 * 0.1, true);
// 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.
/*const provedMintTxStr = await tokenMintWorker.mint(
senderPublicKeyBase58,
noriTokenControllerAddressBase58, // CHECKME @Karol
{
ethDepositProofJson: ethDepositProofJson,
presentationProofStr: presentationJsonStr,
},
1e9 * 0.1,
true
);
console.log('provedMintTxStr', provedMintTxStr);*/
// MOCK for wallet behaviour
/*const { txHash: mintTxHash } =
await tokenMintWorker.WALLET_signAndSend(provedMintTxStr);*/
console.log('mintTxHash', mintTxHash);
console.timeEnd('Minted');
console.log('Minted!');
// Get the amount minted so far and print it
const mintedSoFar = await tokenMintWorker.mintedSoFar(noriTokenControllerAddressBase58, senderPublicKeyBase58);
console.log('mintedSoFar', mintedSoFar);
const balanceOfUser = await tokenMintWorker.getBalanceOf(tokenBaseAddressBase58, senderPublicKeyBase58);
console.log('balanceOfUser', balanceOfUser);
// END MAIN FLOW
}
catch (e) {
throw e;
}
finally {
depositProcessingStatusSubscription.unsubscribe();
}
}, 1000000000);
});
//# sourceMappingURL=e2e.workers2.spec.js.map