feat(cli): withdraw

closes #30, #31
This commit is contained in:
Wanseob Lim
2020-06-23 06:12:27 +09:00
parent e67f5ac90f
commit 39c072fb7b
25 changed files with 408 additions and 107 deletions

View File

@@ -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))
}

View File

@@ -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)

View File

@@ -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",

View File

@@ -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 }
}
}

View File

@@ -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))
}
}

View File

@@ -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> {}

View File

@@ -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 },
}
}
}

View File

@@ -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 },
}
}
}

View File

@@ -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 },
}
}
}

View File

@@ -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 }
}
}

View File

@@ -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 } }

View File

@@ -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 {

View File

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

View File

@@ -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 {

View File

@@ -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
}
}

View File

@@ -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,

View File

@@ -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,
)

View File

@@ -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)

View File

@@ -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.

View File

@@ -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?

View File

@@ -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,

View File

@@ -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]),

View File

@@ -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 {

View File

@@ -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,
},
}),