mirror of
https://github.com/zkopru-network/zkopru.git
synced 2026-04-24 03:00:03 -04:00
@@ -34,7 +34,7 @@ export class ZkAccount {
|
||||
this.snarkPK = Field.from(pk.privateKey)
|
||||
this.ethAccount = pk
|
||||
}
|
||||
this.address = this.ethAccount.address
|
||||
this.address = this.ethAccount.address.toLowerCase()
|
||||
this.pubKey = Point.fromPrivKey(this.snarkPK.toHex(32))
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ describe('unit test', () => {
|
||||
'0xACa94ef8bD5ffEE41947b4585a84BdA5a3d3DA6E',
|
||||
'0x1dF62f291b2E969fB0849d99D9Ce41e2F137006e',
|
||||
]
|
||||
expect(ganacheAddress).toStrictEqual(
|
||||
expect(ganacheAddress.map(a => a.toLowerCase())).toStrictEqual(
|
||||
accounts.map(account => account.address),
|
||||
)
|
||||
}, 30000)
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
"node-ansiparser": "^2.2.0",
|
||||
"node-ansiterminal": "^0.2.1-beta",
|
||||
"node-fetch": "^2.6.0",
|
||||
"soltypes": "^1.2.0",
|
||||
"tar": "^6.0.2",
|
||||
"web3": "^1.2.6",
|
||||
"web3-core": "^1.2.6",
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { TransactionReceipt } from 'web3-core'
|
||||
import { logger } from '@zkopru/utils'
|
||||
import App, { AppMenu, Context } from '..'
|
||||
|
||||
export default class CommitDeposits extends App {
|
||||
static code = AppMenu.COMMIT_DEPOSITS
|
||||
|
||||
async run(context: Context): Promise<{ context: Context; next: number }> {
|
||||
logger.info('Mannual finalization')
|
||||
let receipt!: TransactionReceipt
|
||||
try {
|
||||
receipt = await this.base.commitMassDeposit()
|
||||
} catch (err) {
|
||||
logger.error(err)
|
||||
} finally {
|
||||
if (receipt && receipt.status) {
|
||||
logger.info('Successfully committed the latest mass deposit')
|
||||
} else {
|
||||
logger.error('Failed to commit the latest mass deposit')
|
||||
}
|
||||
}
|
||||
return { context, next: AppMenu.SETUP_MENU }
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,10 @@ import Deposit from './prompts/menus/account-detail-deposit'
|
||||
import DepositEther from './prompts/menus/account-detail-deposit-eth'
|
||||
import TransferEth from './prompts/menus/account-detail-transfer-eth'
|
||||
import TransferMenu from './prompts/menus/account-detail-transfer-menu'
|
||||
import WithdrawRequest from './prompts/menus/account-detail-withdraw-request-menu'
|
||||
import WithdrawRequestEth from './prompts/menus/account-detail-withraw-request-eth'
|
||||
import WithdrawableList from './prompts/menus/account-detail-withdrawable-list'
|
||||
import Withdraw from './prompts/menus/account-detail-withdraw'
|
||||
|
||||
export class WalletDashboard extends Dashboard<Context, ZkWallet> {
|
||||
node: ZkOPRUNode
|
||||
@@ -29,5 +33,12 @@ export class WalletDashboard extends Dashboard<Context, ZkWallet> {
|
||||
this.addPromptApp(AppMenu.DEPOSIT_ETHER, new DepositEther(option))
|
||||
this.addPromptApp(AppMenu.TRANSFER, new TransferMenu(option))
|
||||
this.addPromptApp(AppMenu.TRANSFER_ETH, new TransferEth(option))
|
||||
this.addPromptApp(AppMenu.WITHDRAW_REQUEST, new WithdrawRequest(option))
|
||||
this.addPromptApp(
|
||||
AppMenu.WITHDRAW_REQUEST_ETH,
|
||||
new WithdrawRequestEth(option),
|
||||
)
|
||||
this.addPromptApp(AppMenu.WITHDRAWABLE_LIST, new WithdrawableList(option))
|
||||
this.addPromptApp(AppMenu.WITHDRAW, new Withdraw(option))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { ZkAccount } from '@zkopru/account'
|
||||
import { Sum } from '@zkopru/transaction'
|
||||
import { PromptApp } from '@zkopru/utils'
|
||||
import { ZkWallet, Balance } from '@zkopru/zk-wizard'
|
||||
import { Withdrawal as WithdrawalSql } from '@zkopru/prisma'
|
||||
import { Dashboard } from '../../../dashboard'
|
||||
|
||||
export enum AppMenu {
|
||||
@@ -13,7 +14,15 @@ export enum AppMenu {
|
||||
DEPOSIT_ERC721,
|
||||
TRANSFER,
|
||||
TRANSFER_ETH,
|
||||
TRANSFER_ERC20,
|
||||
TRANSFER_ERC721,
|
||||
WITHDRAW_REQUEST,
|
||||
WITHDRAW_REQUEST_ETH,
|
||||
WITHDRAW_REQUEST_ERC20,
|
||||
WITHDRAW_REQUEST_ERC721,
|
||||
WITHDRAW,
|
||||
INSTANT_WITHDRAW,
|
||||
WITHDRAWABLE_LIST,
|
||||
TOP_MENU = Dashboard.START_CODE,
|
||||
EXIT = Dashboard.EXIT_CODE,
|
||||
}
|
||||
@@ -24,6 +33,7 @@ export interface Context {
|
||||
isReady: boolean
|
||||
balance?: Balance
|
||||
spendables?: Sum
|
||||
withdrawal?: WithdrawalSql
|
||||
}
|
||||
|
||||
export default abstract class App extends PromptApp<Context, ZkWallet> {}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { fromWei } from 'web3-utils'
|
||||
import { Sum } from '@zkopru/transaction'
|
||||
import App, { AppMenu, Context } from '..'
|
||||
|
||||
export default class WithdrawRequest extends App {
|
||||
static code = AppMenu.WITHDRAW_REQUEST
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
async run(context: Context): Promise<{ context: Context; next: number }> {
|
||||
if (!context.account) throw Error('Acocunt is not set')
|
||||
const spendables: Sum = await this.base.getSpendableAmount(context.account)
|
||||
const locked: Sum = await this.base.getLockedAmount(context.account)
|
||||
const { choice } = await this.ask({
|
||||
type: 'select',
|
||||
name: 'choice',
|
||||
initial: 0,
|
||||
message: 'What do you want to do?',
|
||||
choices: [
|
||||
{ title: 'Go back', value: { menu: AppMenu.ACCOUNT_DETAIL } },
|
||||
{
|
||||
title: `Withdraw Ether (balance: ${fromWei(
|
||||
spendables.eth,
|
||||
'ether',
|
||||
)} ETH / locked: ${fromWei(locked.eth, 'ether')} ETH)`,
|
||||
value: { menu: AppMenu.WITHDRAW_REQUEST_ETH },
|
||||
},
|
||||
...Object.keys(spendables.erc20).map(address => ({
|
||||
title: `Withdraw ERC20 ${address} : ${spendables.erc20[address]}`,
|
||||
value: { menu: AppMenu.WITHDRAW_REQUEST_ERC20, address },
|
||||
})),
|
||||
...Object.keys(spendables.erc721).map(address => ({
|
||||
title: `Withdraw NFT ${address} : ${spendables.erc721[address].length}`,
|
||||
value: { menu: AppMenu.WITHDRAW_REQUEST_ERC721, address },
|
||||
})),
|
||||
],
|
||||
})
|
||||
return {
|
||||
next: choice.menu,
|
||||
context: { ...context, address: choice.address },
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import App, { AppMenu, Context } from '..'
|
||||
|
||||
export default class Withdraw extends App {
|
||||
static code = AppMenu.WITHDRAW
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
async run(context: Context): Promise<{ context: Context; next: number }> {
|
||||
if (!context.account) throw Error('Acocunt is not set')
|
||||
if (!context.withdrawal) throw Error('Withdrawal is not set')
|
||||
const result = await this.base.withdraw(context.withdrawal)
|
||||
this.print(`Result: ${result ? 'OK' : 'Failed'}`)
|
||||
return {
|
||||
next: AppMenu.WITHDRAWABLE_LIST,
|
||||
context: { ...context, withdrawal: undefined },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { WithdrawalStatus } from '@zkopru/transaction'
|
||||
import App, { AppMenu, Context } from '..'
|
||||
import { fromWei } from 'web3-utils'
|
||||
|
||||
export default class WithdrawableList extends App {
|
||||
static code = AppMenu.WITHDRAWABLE_LIST
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
async run(context: Context): Promise<{ context: Context; next: number }> {
|
||||
if (!context.account) throw Error('Acocunt is not set')
|
||||
const unfinalized = await this.base.getWithdrawables(
|
||||
context.account,
|
||||
WithdrawalStatus.UNFINALIZED,
|
||||
)
|
||||
const finalized = await this.base.getWithdrawables(
|
||||
context.account,
|
||||
WithdrawalStatus.WITHDRAWABLE,
|
||||
)
|
||||
const { choice } = await this.ask({
|
||||
type: 'select',
|
||||
name: 'choice',
|
||||
initial: 0,
|
||||
message: 'What do you want to do?',
|
||||
choices: [
|
||||
{
|
||||
title: 'Go back',
|
||||
value: { menu: AppMenu.ACCOUNT_DETAIL, withdrawal: undefined },
|
||||
},
|
||||
...unfinalized.map(w => ({
|
||||
title: `Not finalized yet - instant withdraw?
|
||||
ETH: ${fromWei(w.eth, 'ether')}
|
||||
ERC20: ${w.erc20Amount}
|
||||
NFT: ${w.nft}`,
|
||||
value: { menu: AppMenu.INSTANT_WITHDRAW, withdrawal: w },
|
||||
})),
|
||||
...finalized.map(w => ({
|
||||
title: `finalized - withdraw now?
|
||||
ETH: ${fromWei(w.eth, 'ether')}
|
||||
ERC20: ${w.erc20Amount}
|
||||
NFT: ${w.nft}`,
|
||||
value: { menu: AppMenu.WITHDRAW, withdrawal: w },
|
||||
})),
|
||||
],
|
||||
})
|
||||
return {
|
||||
next: choice.menu,
|
||||
context: { ...context, withdrawal: choice.withdrawal },
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import { Point, Field } from '@zkopru/babyjubjub'
|
||||
import { Sum, TxBuilder, RawTx, Utxo } from '@zkopru/transaction'
|
||||
import { parseStringToUnit, logger } from '@zkopru/utils'
|
||||
import { fromWei, toBN, toWei, isAddress } from 'web3-utils'
|
||||
import { Address } from 'soltypes'
|
||||
import App, { AppMenu, Context } from '..'
|
||||
|
||||
export default class WithdrawRequestEth extends App {
|
||||
static code = AppMenu.WITHDRAW_REQUEST_ETH
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
async run(context: Context): Promise<{ context: Context; next: number }> {
|
||||
const wallet = this.base
|
||||
const { account } = context
|
||||
if (!account) throw Error('Acocunt is not set')
|
||||
const spendables: Utxo[] = await wallet.getSpendables(account)
|
||||
const spendableAmount = Sum.from(spendables)
|
||||
let weiPerByte!: string
|
||||
try {
|
||||
weiPerByte = await wallet.fetchPrice()
|
||||
} catch (err) {
|
||||
logger.error('price fetch error')
|
||||
throw err
|
||||
}
|
||||
const regularPrice = fromWei(
|
||||
toBN(weiPerByte || '0')
|
||||
.muln(566) // 566 : for 2 inputs & 2 outputs
|
||||
.toString(),
|
||||
'ether',
|
||||
)
|
||||
const messages: string[] = []
|
||||
messages.push(`Account: ${account.pubKey.toHex()}`)
|
||||
messages.push(`Withdrawable ETH: ${fromWei(spendableAmount.eth, 'ether')}`)
|
||||
messages.push(
|
||||
`Recommended fee per byte: ${fromWei(weiPerByte, 'gwei')} gwei / byte`,
|
||||
)
|
||||
messages.push(
|
||||
`You may spend ${regularPrice} ETH to send a 566 bytes size tx.`,
|
||||
)
|
||||
this.print(messages.join('\n'))
|
||||
let amountWei: string
|
||||
let confirmedWeiPerByte: string
|
||||
let tx!: RawTx
|
||||
let to!: Address
|
||||
do {
|
||||
const msgs: string[] = []
|
||||
const { address } = await this.ask({
|
||||
type: 'text',
|
||||
name: 'address',
|
||||
initial: '0xabcdef0123456789abcdef0123456789abcdef012',
|
||||
message: 'Withdraw out to? (Ethereum address)',
|
||||
})
|
||||
try {
|
||||
to = Address.from(address)
|
||||
} catch (err) {
|
||||
logger.error(`Failed to get address from`)
|
||||
logger.error(err)
|
||||
}
|
||||
if (!isAddress(to.toString())) {
|
||||
this.print('Provided invalid Ethereum address')
|
||||
// eslint-disable-next-line no-continue
|
||||
continue
|
||||
}
|
||||
const { amount } = await this.ask({
|
||||
type: 'text',
|
||||
name: 'amount',
|
||||
initial: 0,
|
||||
message: 'How much ETH do you want to transfer(ex: 0.3 ETH)?',
|
||||
})
|
||||
const eth = parseStringToUnit(amount, 'ether')
|
||||
amountWei = toWei(eth.val, eth.unit).toString()
|
||||
msgs.push(`Sending amount: ${fromWei(amountWei, 'ether')} ETH`)
|
||||
msgs.push(` = ${amountWei} wei`)
|
||||
this.print([...messages, ...msgs].join('\n'))
|
||||
const gweiPerByte = fromWei(weiPerByte, 'gwei')
|
||||
const { fee } = await this.ask({
|
||||
type: 'text',
|
||||
name: 'fee',
|
||||
initial: `${gweiPerByte} gwei`,
|
||||
message: `Fee per byte. ex) ${gweiPerByte} gwei`,
|
||||
})
|
||||
const confirmedWei = parseStringToUnit(fee, 'gwei')
|
||||
confirmedWeiPerByte = toWei(confirmedWei.val, confirmedWei.unit)
|
||||
msgs.push(`Wei per byte: ${fromWei(confirmedWeiPerByte, 'ether')} ETH`)
|
||||
msgs.push(` = ${fromWei(confirmedWeiPerByte, 'gwei')} gwei`)
|
||||
this.print(messages.join('\n'))
|
||||
const { prePayFee } = await this.ask({
|
||||
type: 'text',
|
||||
name: 'prePayFee',
|
||||
initial: `0 gwei`,
|
||||
message: `Additional fee for instant withdrawal.`,
|
||||
})
|
||||
const confirmedPrePayFee = parseStringToUnit(prePayFee, 'gwei')
|
||||
const confirmedPrePayFeeToWei = toWei(
|
||||
confirmedPrePayFee.val,
|
||||
confirmedPrePayFee.unit,
|
||||
)
|
||||
msgs.push(
|
||||
`Instant withdrawal fee: ${fromWei(
|
||||
confirmedPrePayFeeToWei,
|
||||
'ether',
|
||||
)} ETH`,
|
||||
)
|
||||
msgs.push(` = ${fromWei(confirmedPrePayFeeToWei, 'gwei')} gwei`)
|
||||
this.print(messages.join('\n'))
|
||||
|
||||
const txBuilder = TxBuilder.from(account.pubKey)
|
||||
try {
|
||||
tx = txBuilder
|
||||
.provide(...spendables.map(note => Utxo.from(note)))
|
||||
.weiPerByte(confirmedWeiPerByte)
|
||||
.sendEther({
|
||||
eth: Field.from(amountWei),
|
||||
to: Point.zero,
|
||||
withdrawal: {
|
||||
to: Field.from(to.toString()),
|
||||
fee: Field.from(confirmedPrePayFeeToWei),
|
||||
},
|
||||
})
|
||||
.build()
|
||||
this.print(`Succeeded to build a transaction. Start to generate proof`)
|
||||
} catch (err) {
|
||||
this.print(`Failed to build transaction \n${err.toString()}`)
|
||||
}
|
||||
} while (!tx)
|
||||
|
||||
try {
|
||||
await wallet.sendTx(tx, account)
|
||||
} catch (err) {
|
||||
logger.error(err)
|
||||
logger.error(tx)
|
||||
}
|
||||
return { context, next: AppMenu.TRANSFER }
|
||||
}
|
||||
}
|
||||
@@ -54,7 +54,8 @@ export default class AccountDetail extends App {
|
||||
{ title: 'Go to top menu', value: AppMenu.TOP_MENU },
|
||||
{ title: 'Deposit', value: AppMenu.DEPOSIT },
|
||||
{ title: 'Transfer', value: AppMenu.TRANSFER },
|
||||
{ title: 'Withdraw', value: AppMenu.WITHDRAW },
|
||||
{ title: 'Withdraw request', value: AppMenu.WITHDRAW_REQUEST },
|
||||
{ title: 'Withdraw out', value: AppMenu.WITHDRAWABLE_LIST },
|
||||
],
|
||||
})
|
||||
return { next: choice, context: { ...context, balance } }
|
||||
|
||||
@@ -59,7 +59,6 @@ contract Coordinatable is Layer2 {
|
||||
Layer2.chain.proposals[_block.checksum] = Proposal(
|
||||
currentBlockHash,
|
||||
block.number + CHALLENGE_PERIOD,
|
||||
false,
|
||||
false
|
||||
);
|
||||
/// Record l2 chain
|
||||
@@ -101,7 +100,7 @@ contract Coordinatable is Layer2 {
|
||||
require(finalization.massMigrations.root() == finalization.header.migrationRoot, "Submitted different deposit root");
|
||||
require(finalization.header.hash() == proposal.headerHash, "Invalid header data");
|
||||
require(!proposal.slashed, "Slashed roll up can't be finalized");
|
||||
require(!proposal.finalized, "Already finalized");
|
||||
require(!Layer2.chain.finalized[proposal.headerHash], "Already finalized");
|
||||
require(finalization.header.parentBlock == Layer2.chain.latest, "The latest block should be its parent");
|
||||
require(finalization.header.parentBlock != proposal.headerHash, "Reentrancy case");
|
||||
|
||||
@@ -131,9 +130,10 @@ contract Coordinatable is Layer2 {
|
||||
proposer.reward += finalization.header.fee;
|
||||
|
||||
/// Update the chain
|
||||
proposal.finalized = true;
|
||||
Layer2.chain.finalized[proposal.headerHash] = true;
|
||||
Layer2.chain.latest = proposal.headerHash;
|
||||
emit Finalized(proposal.headerHash);
|
||||
delete Layer2.chain.proposals[finalization.proposalChecksum];
|
||||
}
|
||||
|
||||
function withdrawReward(uint amount) public {
|
||||
|
||||
@@ -167,14 +167,9 @@ contract UserInteractable is Layer2 {
|
||||
uint[] memory siblings
|
||||
) internal {
|
||||
require(nft*amount == 0, "Only ERC20 or ERC721");
|
||||
require(Layer2.chain.proposals[blockHash].finalized, "Not a finalized block");
|
||||
require(Layer2.chain.finalized[blockHash], "Not a finalized block");
|
||||
uint256 root = Layer2.chain.withdrawalRootOf[blockHash];
|
||||
bytes32 currentBlock = blockHash;
|
||||
while (root == 0) {
|
||||
currentBlock = Layer2.chain.parentOf[currentBlock];
|
||||
root = Layer2.chain.withdrawalRootOf[currentBlock];
|
||||
// if the block does not contain any withdrawal, it does not record withdrawal root
|
||||
}
|
||||
bytes32 withdrawalHash = _withdrawalHash(
|
||||
note,
|
||||
owner,
|
||||
@@ -200,7 +195,7 @@ contract UserInteractable is Layer2 {
|
||||
);
|
||||
require(inclusion, "The given withdrawal note does not exist");
|
||||
/// Withdraw ETH & get fee
|
||||
if(eth!=0) {
|
||||
if(eth != 0) {
|
||||
if(to == msg.sender) {
|
||||
payable(to).transfer(eth + fee);
|
||||
} else {
|
||||
@@ -209,10 +204,12 @@ contract UserInteractable is Layer2 {
|
||||
}
|
||||
}
|
||||
/// Withdrawn token
|
||||
if(amount!=0) {
|
||||
IERC20(token).transfer(to, amount);
|
||||
} else {
|
||||
IERC721(token).transferFrom(address(this), to, nft);
|
||||
if (token != address(0)) {
|
||||
if (amount != 0) {
|
||||
IERC20(token).transfer(to, amount);
|
||||
} else {
|
||||
IERC721(token).transferFrom(address(this), to, nft);
|
||||
}
|
||||
}
|
||||
/// Mark as withdrawn
|
||||
Layer2.chain.withdrawn[withdrawalHash] = true;
|
||||
|
||||
@@ -6,12 +6,13 @@ struct Blockchain {
|
||||
bytes32 genesis;
|
||||
bytes32 latest;
|
||||
/** For inclusion reference */
|
||||
mapping(bytes32=>bytes32) parentOf; // childBlockHash=>parentBlockHash
|
||||
mapping(bytes32=>uint256) utxoRootOf; // header => utxoRoot
|
||||
mapping(bytes32=>bytes32) parentOf; // childBlockHash => parentBlockHash
|
||||
mapping(bytes32=>uint256) utxoRootOf; // blockhash => utxoRoot
|
||||
mapping(uint256=>bool) finalizedUTXORoots; // all finalized utxo roots
|
||||
/** For coordinating */
|
||||
mapping(address=>Proposer) proposers;
|
||||
mapping(bytes32=>Proposal) proposals;
|
||||
mapping(bytes32=>bool) finalized; // blockhash => finalized?
|
||||
/** For deposit */
|
||||
MassDeposit stagedDeposits;
|
||||
uint stagedSize;
|
||||
@@ -79,7 +80,6 @@ struct Proposal {
|
||||
bytes32 headerHash;
|
||||
uint challengeDue;
|
||||
bool slashed;
|
||||
bool finalized;
|
||||
}
|
||||
|
||||
struct Challenge {
|
||||
|
||||
@@ -403,7 +403,9 @@ export class Coordinator extends EventEmitter {
|
||||
return
|
||||
}
|
||||
if (this.node.status !== NetworkStatus.FULLY_SYNCED) {
|
||||
logger.trace('Skip gen block. Syncing layer 2 with the layer 1')
|
||||
logger.trace(
|
||||
`Skip gen block. Syncing layer 2 with the layer 1 - status: ${this.node.status}`,
|
||||
)
|
||||
return
|
||||
}
|
||||
let block: {
|
||||
@@ -624,16 +626,6 @@ export class Coordinator extends EventEmitter {
|
||||
if (!finalization) return
|
||||
logger.info('finalization')
|
||||
const blockHash = headerHash(finalization.header).toString()
|
||||
|
||||
const mdHash = soliditySha3Raw(
|
||||
finalization.massDeposits[0].merged.toString(),
|
||||
finalization.massDeposits[0].fee.toString(),
|
||||
)
|
||||
logger.debug(`massdeposit hash: ${mdHash}`)
|
||||
const exist = await this.node.l1Contract.upstream.methods
|
||||
.committedDeposits(mdHash)
|
||||
.call()
|
||||
logger.debug(`mass deposit exist: ${exist}`)
|
||||
const tx = this.node.l1Contract.coordinator.methods.finalize(
|
||||
`0x${serializeFinalization(finalization).toString('hex')}`,
|
||||
)
|
||||
@@ -690,9 +682,6 @@ export class Coordinator extends EventEmitter {
|
||||
const block = Block.fromTx(tx, true)
|
||||
|
||||
const finalization: Finalization = block.getFinalization()
|
||||
logger.debug(
|
||||
`merged: ${finalization.massDeposits[0].merged} / fee: ${finalization.massDeposits[0].fee}`,
|
||||
)
|
||||
return finalization
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,7 +282,7 @@ export class L2Chain {
|
||||
|
||||
async findMyUtxos(txs: ZkTx[], accounts: ZkAccount[]) {
|
||||
const txsWithMemo = txs.filter(tx => tx.memo)
|
||||
logger.info(`findMyNotes`)
|
||||
logger.info(`findMyUtxos`)
|
||||
const myUtxos: Utxo[] = []
|
||||
for (const tx of txsWithMemo) {
|
||||
for (const account of accounts) {
|
||||
@@ -325,6 +325,7 @@ export class L2Chain {
|
||||
}
|
||||
|
||||
async findMyWithdrawals(txs: ZkTx[], accounts: ZkAccount[]) {
|
||||
logger.info(`findMyWithdrawals`)
|
||||
const outflows = txs.reduce(
|
||||
(acc, tx) => [
|
||||
...acc,
|
||||
@@ -334,11 +335,29 @@ export class L2Chain {
|
||||
],
|
||||
[] as ZkOutflow[],
|
||||
)
|
||||
logger.debug(
|
||||
`withdrawal address =>
|
||||
${outflows.map(outflow =>
|
||||
outflow.data?.to
|
||||
.toAddress()
|
||||
.toString()
|
||||
.toLowerCase(),
|
||||
)}`,
|
||||
)
|
||||
logger.debug(
|
||||
`my address =>${accounts.map(account => account.address.toLowerCase())}`,
|
||||
)
|
||||
const myWithdrawalOutputs: ZkOutflow[] = outflows.filter(
|
||||
outflow =>
|
||||
outflow.data &&
|
||||
outflow.data?.to.toAddress().toString() in
|
||||
accounts.map(account => account.address),
|
||||
accounts
|
||||
.map(account => account.address.toLowerCase())
|
||||
.includes(
|
||||
outflow.data?.to
|
||||
.toAddress()
|
||||
.toString()
|
||||
.toLowerCase(),
|
||||
),
|
||||
)
|
||||
// TODO needs batch transaction
|
||||
for (const output of myWithdrawalOutputs) {
|
||||
@@ -357,6 +376,7 @@ export class L2Chain {
|
||||
fee: output.data.fee.toUint256().toString(),
|
||||
status: WithdrawalStatus.WITHDRAWABLE,
|
||||
}
|
||||
logger.info(`found my withdrawal: ${withdrawalSql.hash}`)
|
||||
await this.db.write(prisma =>
|
||||
prisma.withdrawal.upsert({
|
||||
where: { hash: withdrawalSql.hash },
|
||||
@@ -502,11 +522,9 @@ export class L2Chain {
|
||||
) {
|
||||
const myWithdrawals = withdrawals.filter(w => w.shouldTrack)
|
||||
for (const withdrawal of myWithdrawals) {
|
||||
const merkleProof = await this.grove.withdrawalMerkleProof(
|
||||
withdrawal.hash,
|
||||
)
|
||||
const { noteHash } = withdrawal
|
||||
if (!noteHash) throw Error('Withdrawal does not have note hash')
|
||||
const merkleProof = await this.grove.withdrawalMerkleProof(noteHash)
|
||||
await this.db.write(prisma =>
|
||||
prisma.withdrawal.update({
|
||||
where: { hash: noteHash.toString() },
|
||||
@@ -535,7 +553,6 @@ export class L2Chain {
|
||||
const withdrawalHashes: { noteHash: Field; withdrawalHash: Uint256 }[] = []
|
||||
for (const tx of block.body.txs) {
|
||||
for (const outflow of tx.outflow) {
|
||||
logger.debug(`outflow type ${outflow.outflowType.toString()}`)
|
||||
if (outflow.outflowType.eqn(OutflowType.UTXO)) {
|
||||
utxoHashes.push(outflow.note)
|
||||
} else if (outflow.outflowType.eqn(OutflowType.WITHDRAWAL)) {
|
||||
@@ -550,7 +567,6 @@ export class L2Chain {
|
||||
}
|
||||
}
|
||||
}
|
||||
logger.debug(`utxo list.. ${utxoHashes}`)
|
||||
const myUtxoList = await this.db.read(prisma =>
|
||||
prisma.utxo.findMany({
|
||||
where: {
|
||||
@@ -570,13 +586,11 @@ export class L2Chain {
|
||||
const shouldTrack: { [key: string]: boolean } = {}
|
||||
for (const myNote of myUtxoList) {
|
||||
shouldTrack[myNote.hash] = true
|
||||
logger.debug(`found my note: ${myNote.hash}`)
|
||||
}
|
||||
for (const myNote of myWithdrawalList) {
|
||||
shouldTrack[myNote.hash] = true
|
||||
}
|
||||
for (const output of utxoHashes) {
|
||||
logger.debug(`utxo...: ${output.toString(10)}`)
|
||||
const trackThisNote = shouldTrack[output.toString(10)]
|
||||
utxos.push({
|
||||
hash: output,
|
||||
|
||||
@@ -11,6 +11,7 @@ import { BootstrapHelper } from './bootstrap'
|
||||
import { Block, headerHash } from './block'
|
||||
import { Synchronizer } from './synchronizer'
|
||||
import { ZkOPRUNode } from './zkopru-node'
|
||||
import { logger } from '@zkopru/utils'
|
||||
|
||||
type provider = WebsocketProvider | IpcProvider
|
||||
|
||||
@@ -62,6 +63,10 @@ export class LightNode extends ZkOPRUNode {
|
||||
return
|
||||
}
|
||||
const bootstrapData = await this.bootstrapHelper.fetchBootstrapData(latest)
|
||||
if (!bootstrapData.proposal.proposalTx) {
|
||||
logger.error('bootstrap api is not giving proposalTx')
|
||||
return
|
||||
}
|
||||
const proposalData = await this.l1Contract.web3.eth.getTransaction(
|
||||
bootstrapData.proposal.proposalTx,
|
||||
)
|
||||
|
||||
@@ -307,10 +307,14 @@ export class Synchronizer {
|
||||
if (typeof event.returnValues === 'string')
|
||||
blockHash = event.returnValues
|
||||
else blockHash = (event.returnValues as any).blockHash
|
||||
const hash = Bytes32.from(blockHash).toString()
|
||||
logger.debug(`finalization hash@!${hash}`)
|
||||
logger.debug(`${JSON.stringify(event.returnValues)}`)
|
||||
await this.db.write(prisma =>
|
||||
prisma.proposal.update({
|
||||
where: { hash: Bytes32.from(blockHash).toString() },
|
||||
data: { finalized: true },
|
||||
prisma.proposal.upsert({
|
||||
where: { hash },
|
||||
create: { hash, finalized: true },
|
||||
update: { finalized: true },
|
||||
}),
|
||||
)
|
||||
if (cb) cb(blockHash)
|
||||
|
||||
@@ -6,6 +6,7 @@ import { logger } from '@zkopru/utils'
|
||||
import { Uint256 } from 'soltypes'
|
||||
import { scheduleJob, Job } from 'node-schedule'
|
||||
import { EventEmitter } from 'events'
|
||||
import assert from 'assert'
|
||||
import { L1Contract } from './layer1'
|
||||
import { Verifier, VerifyOption } from './verifier'
|
||||
import { L2Chain } from './layer2'
|
||||
@@ -221,7 +222,7 @@ export class ZkOPRUNode extends EventEmitter {
|
||||
take: 1,
|
||||
}),
|
||||
)
|
||||
if (verifiedProposals[0]) {
|
||||
if (verifiedProposals[0] && verifiedProposals[0].proposalNum !== null) {
|
||||
this.setLatestProcessed(verifiedProposals[0].proposalNum + 1)
|
||||
}
|
||||
}
|
||||
@@ -280,7 +281,7 @@ export class ZkOPRUNode extends EventEmitter {
|
||||
if (availableFetchJob === 0) return
|
||||
const candidates = await this.db.read(prisma =>
|
||||
prisma.proposal.findMany({
|
||||
where: { fetched: null },
|
||||
where: { fetched: null, proposalTx: { not: null } },
|
||||
orderBy: {
|
||||
proposalNum: 'asc',
|
||||
},
|
||||
@@ -289,6 +290,7 @@ export class ZkOPRUNode extends EventEmitter {
|
||||
)
|
||||
|
||||
candidates.forEach(candidate => {
|
||||
assert(candidate.proposalTx)
|
||||
this.fetch(candidate.proposalTx)
|
||||
})
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -53,9 +53,9 @@ model Config {
|
||||
|
||||
model Proposal {
|
||||
hash String @id
|
||||
proposalNum Int
|
||||
proposedAt Int
|
||||
proposalTx String // tx hash
|
||||
proposalNum Int?
|
||||
proposedAt Int?
|
||||
proposalTx String? // tx hash
|
||||
proposalData String? // stringified json
|
||||
fetched String?
|
||||
finalized Boolean?
|
||||
|
||||
@@ -7,6 +7,7 @@ export { Note, OutflowType } from './note'
|
||||
export { TxBuilder, RawTx } from './tx'
|
||||
export { ZkTx, ZkInflow, ZkOutflow, PublicData, SNARK } from './zk_tx'
|
||||
export { Sum } from './note-sum'
|
||||
export { Outflow } from './outflow'
|
||||
|
||||
export const TokenUtils = {
|
||||
DAI,
|
||||
|
||||
@@ -79,7 +79,7 @@ export class TxBuilder {
|
||||
'You should have only one value of withdrawalTo or migrationTo',
|
||||
)
|
||||
const note = Utxo.newEtherNote({ eth, pubKey: to })
|
||||
this.send(note)
|
||||
this.send(note, withdrawal, migration)
|
||||
return this
|
||||
}
|
||||
|
||||
@@ -183,6 +183,13 @@ export class TxBuilder {
|
||||
const spendables: Utxo[] = [...this.spendables]
|
||||
const spendings: Utxo[] = []
|
||||
const sendingAmount = Sum.from(this.sendings)
|
||||
const outgoingNotes: (Withdrawal | Migration)[] = this.sendings.filter(
|
||||
sending => sending instanceof Withdrawal || sending instanceof Migration,
|
||||
) as (Withdrawal | Migration)[]
|
||||
const l1Fee = outgoingNotes.reduce(
|
||||
(acc, note) => acc.add(note.publicData.fee),
|
||||
Field.zero,
|
||||
)
|
||||
|
||||
// Find ERC20 notes to spend
|
||||
Object.keys(sendingAmount.erc20).forEach(addr => {
|
||||
@@ -282,7 +289,7 @@ export class TxBuilder {
|
||||
}
|
||||
|
||||
const getRequiredETH = (): Field => {
|
||||
return sendingAmount.eth.add(getTxFee())
|
||||
return sendingAmount.eth.add(getTxFee()).add(l1Fee)
|
||||
}
|
||||
|
||||
// Spend ETH containing notes until it hits the number
|
||||
@@ -318,7 +325,10 @@ export class TxBuilder {
|
||||
const outflow = [...this.sendings, ...changes]
|
||||
const inflowSum = Sum.from(inflow)
|
||||
const outflowSum = Sum.from(outflow)
|
||||
assert(inflowSum.eth.eq(outflowSum.eth.add(finalFee)), 'inflow != outflow')
|
||||
assert(
|
||||
inflowSum.eth.eq(outflowSum.eth.add(finalFee).add(l1Fee)),
|
||||
'inflow != outflow',
|
||||
)
|
||||
for (const addr of Object.keys(inflowSum.erc20)) {
|
||||
assert(
|
||||
inflowSum.erc20[addr].eq(outflowSum.erc20[addr]),
|
||||
|
||||
@@ -176,45 +176,33 @@ export class ZkTx {
|
||||
}
|
||||
|
||||
signals(): BigInteger[] {
|
||||
const signals = [
|
||||
this.fee.toIden3BigInt(),
|
||||
this.swap ? this.swap.toIden3BigInt() : Field.zero.toIden3BigInt(),
|
||||
...this.inflow.map(inflow => inflow.root.toIden3BigInt()),
|
||||
...this.inflow.map(inflow => inflow.nullifier.toIden3BigInt()),
|
||||
...this.outflow.map(outflow => outflow.note.toIden3BigInt()),
|
||||
...this.outflow.map(outflow => outflow.outflowType.toIden3BigInt()),
|
||||
const signals: Field[] = [
|
||||
this.fee,
|
||||
this.swap ? this.swap : Field.zero,
|
||||
...this.inflow.map(inflow => inflow.root),
|
||||
...this.inflow.map(inflow => inflow.nullifier),
|
||||
...this.outflow.map(outflow => outflow.note),
|
||||
...this.outflow.map(outflow => outflow.outflowType),
|
||||
...this.outflow.map(outflow =>
|
||||
outflow.data
|
||||
? outflow.data.to.toIden3BigInt()
|
||||
: Field.zero.toIden3BigInt(),
|
||||
outflow.data ? outflow.data.to : Field.zero,
|
||||
),
|
||||
...this.outflow.map(outflow =>
|
||||
outflow.data
|
||||
? outflow.data.eth.toIden3BigInt()
|
||||
: Field.zero.toIden3BigInt(),
|
||||
outflow.data ? outflow.data.eth : Field.zero,
|
||||
),
|
||||
...this.outflow.map(outflow =>
|
||||
outflow.data
|
||||
? outflow.data.tokenAddr.toIden3BigInt()
|
||||
: Field.zero.toIden3BigInt(),
|
||||
outflow.data ? outflow.data.tokenAddr : Field.zero,
|
||||
),
|
||||
...this.outflow.map(outflow =>
|
||||
outflow.data
|
||||
? outflow.data.erc20Amount.toIden3BigInt()
|
||||
: Field.zero.toIden3BigInt(),
|
||||
outflow.data ? outflow.data.erc20Amount : Field.zero,
|
||||
),
|
||||
...this.outflow.map(outflow =>
|
||||
outflow.data
|
||||
? outflow.data.nft.toIden3BigInt()
|
||||
: Field.zero.toIden3BigInt(),
|
||||
outflow.data ? outflow.data.nft : Field.zero,
|
||||
),
|
||||
...this.outflow.map(outflow =>
|
||||
outflow.data
|
||||
? outflow.data.fee.toIden3BigInt()
|
||||
: Field.zero.toIden3BigInt(),
|
||||
outflow.data ? outflow.data.fee : Field.zero,
|
||||
),
|
||||
]
|
||||
return signals
|
||||
return signals.map(f => f.toIden3BigInt())
|
||||
}
|
||||
|
||||
static decode(buff: Buffer): ZkTx {
|
||||
|
||||
@@ -3,9 +3,9 @@ import {
|
||||
Sum,
|
||||
RawTx,
|
||||
Utxo,
|
||||
Note,
|
||||
WithdrawalStatus,
|
||||
Withdrawal,
|
||||
Outflow,
|
||||
} from '@zkopru/transaction'
|
||||
import { Field, F, Point } from '@zkopru/babyjubjub'
|
||||
import { Layer1 } from '@zkopru/contracts'
|
||||
@@ -372,6 +372,7 @@ export class ZkWallet {
|
||||
)
|
||||
const receipt = await this.node.l1Contract.sendTx(tx, {
|
||||
from: this.account.address,
|
||||
gas: 100000,
|
||||
})
|
||||
if (receipt) {
|
||||
await this.db.write(prisma =>
|
||||
@@ -455,14 +456,13 @@ export class ZkWallet {
|
||||
const { verifier } = this.node
|
||||
const snarkValid = await verifier.snarkVerifier.verifyTx(zkTx)
|
||||
assert(snarkValid, 'generated snark proof is invalid')
|
||||
assert(zkTx.memo, 'memo does not exist')
|
||||
const response = await fetch(`${this.coordinator}/tx`, {
|
||||
method: 'post',
|
||||
body: zkTx.encode().toString('hex'),
|
||||
})
|
||||
// add newly created notes
|
||||
for (const note of tx.outflow) {
|
||||
await this.saveNote(note)
|
||||
for (const outflow of tx.outflow) {
|
||||
await this.saveOutflow(outflow)
|
||||
}
|
||||
// mark used notes as spending
|
||||
await this.db.write(prisma =>
|
||||
@@ -508,52 +508,52 @@ export class ZkWallet {
|
||||
from: this.account.address,
|
||||
value: note.eth.add(fee).toString(),
|
||||
})
|
||||
await this.saveNote(note)
|
||||
await this.saveOutflow(note)
|
||||
// TODO check what web3 methods returns when it failes
|
||||
if (receipt) return true
|
||||
return false
|
||||
}
|
||||
|
||||
private async saveNote(note: Note) {
|
||||
if (note instanceof Utxo) {
|
||||
private async saveOutflow(outflow: Outflow) {
|
||||
if (outflow instanceof Utxo) {
|
||||
await this.db.write(prisma =>
|
||||
prisma.utxo.create({
|
||||
data: {
|
||||
hash: note
|
||||
hash: outflow
|
||||
.hash()
|
||||
.toUint256()
|
||||
.toString(),
|
||||
eth: note.eth.toUint256().toString(),
|
||||
pubKey: Bytes32.from(note.pubKey.toHex()).toString(),
|
||||
salt: note.salt.toUint256().toString(),
|
||||
tokenAddr: note.tokenAddr.toAddress().toString(),
|
||||
erc20Amount: note.erc20Amount.toUint256().toString(),
|
||||
nft: note.nft.toUint256().toString(),
|
||||
eth: outflow.eth.toUint256().toString(),
|
||||
pubKey: Bytes32.from(outflow.pubKey.toHex()).toString(),
|
||||
salt: outflow.salt.toUint256().toString(),
|
||||
tokenAddr: outflow.tokenAddr.toAddress().toString(),
|
||||
erc20Amount: outflow.erc20Amount.toUint256().toString(),
|
||||
nft: outflow.nft.toUint256().toString(),
|
||||
status: UtxoStatus.NON_INCLUDED,
|
||||
nullifier: note
|
||||
nullifier: outflow
|
||||
.nullifier()
|
||||
.toUint256()
|
||||
.toString(),
|
||||
},
|
||||
}),
|
||||
)
|
||||
} else if (note instanceof Withdrawal) {
|
||||
} else if (outflow instanceof Withdrawal) {
|
||||
await this.db.write(prisma =>
|
||||
prisma.withdrawal.create({
|
||||
data: {
|
||||
hash: note
|
||||
hash: outflow
|
||||
.hash()
|
||||
.toUint256()
|
||||
.toString(),
|
||||
withdrawalHash: note.withdrawalHash().toString(),
|
||||
eth: note.eth.toUint256().toString(),
|
||||
pubKey: Bytes32.from(note.pubKey.toHex()).toString(),
|
||||
salt: note.salt.toUint256().toString(),
|
||||
tokenAddr: note.tokenAddr.toAddress().toString(),
|
||||
to: note.publicData.to.toAddress().toString(),
|
||||
fee: note.publicData.fee.toAddress().toString(),
|
||||
erc20Amount: note.erc20Amount.toUint256().toString(),
|
||||
nft: note.nft.toUint256().toString(),
|
||||
withdrawalHash: outflow.withdrawalHash().toString(),
|
||||
eth: outflow.eth.toUint256().toString(),
|
||||
pubKey: Bytes32.from(outflow.pubKey.toHex()).toString(),
|
||||
salt: outflow.salt.toUint256().toString(),
|
||||
tokenAddr: outflow.tokenAddr.toAddress().toString(),
|
||||
to: outflow.publicData.to.toAddress().toString(),
|
||||
fee: outflow.publicData.fee.toAddress().toString(),
|
||||
erc20Amount: outflow.erc20Amount.toUint256().toString(),
|
||||
nft: outflow.nft.toUint256().toString(),
|
||||
status: UtxoStatus.NON_INCLUDED,
|
||||
},
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user