UNPKG

zkcloudworker

Version:
442 lines 18.1 kB
import { Whitelist } from "./whitelist.js"; import { fetchMinaAccount } from "../utils/fetch.js"; import { accountBalanceMina } from "../utils/mina.js"; import { FungibleToken, WhitelistedFungibleToken, FungibleTokenAdmin, FungibleTokenWhitelistedAdmin, FungibleTokenOfferContract, FungibleTokenBidContract, tokenVerificationKeys, } from "./token.js"; import { PublicKey, Mina, AccountUpdate, UInt8, Bool, Struct, Field, } from "o1js"; export async function buildTokenDeployTransaction(params) { const { fee, sender, nonce, memo, tokenAddress, adminContractAddress, uri, symbol, developerAddress, developerFee, provingKey, provingFee, decimals, chain, } = params; const isWhitelisted = params.whitelist !== undefined; if (memo && typeof memo !== "string") throw new Error("Memo must be a string"); if (memo && memo.length > 30) throw new Error("Memo must be less than 30 characters"); if (!symbol || typeof symbol !== "string") throw new Error("Symbol must be a string"); if (symbol.length >= 7) throw new Error("Symbol must be less than 7 characters"); const adminContract = isWhitelisted ? FungibleTokenWhitelistedAdmin : FungibleTokenAdmin; const tokenContract = isWhitelisted ? WhitelistedFungibleToken : FungibleToken; const vk = tokenVerificationKeys[chain === "mainnet" ? "mainnet" : "testnet"].vk; if (!vk || !vk.FungibleTokenWhitelistedAdmin || !vk.FungibleTokenWhitelistedAdmin.hash || !vk.FungibleTokenWhitelistedAdmin.data || !vk.FungibleTokenAdmin || !vk.FungibleTokenAdmin.hash || !vk.FungibleTokenAdmin.data || !vk.WhitelistedFungibleToken || !vk.WhitelistedFungibleToken.hash || !vk.WhitelistedFungibleToken.data || !vk.FungibleToken || !vk.FungibleToken.hash || !vk.FungibleToken.data) throw new Error("Cannot get verification keys"); const adminVerificationKey = isWhitelisted ? vk.FungibleTokenWhitelistedAdmin : vk.FungibleTokenAdmin; const tokenVerificationKey = isWhitelisted ? vk.WhitelistedFungibleToken : vk.FungibleToken; if (!adminVerificationKey || !tokenVerificationKey) throw new Error("Cannot get verification keys"); await fetchMinaAccount({ publicKey: sender, force: true, }); if (!Mina.hasAccount(sender)) { throw new Error("Sender does not have account"); } console.log("Sender balance:", await accountBalanceMina(sender)); const whitelist = params.whitelist ? typeof params.whitelist === "string" ? Whitelist.fromString(params.whitelist) : await Whitelist.create({ list: params.whitelist, name: symbol }) : undefined; const zkToken = new tokenContract(tokenAddress); const zkAdmin = new adminContract(adminContractAddress); const tx = await Mina.transaction({ sender, fee, memo: memo ?? `deploy ${symbol}`, nonce }, async () => { const feeAccountUpdate = AccountUpdate.createSigned(sender); feeAccountUpdate.balance.subInPlace(3_000_000_000); feeAccountUpdate.send({ to: provingKey, amount: provingFee, }); if (developerAddress && developerFee) { feeAccountUpdate.send({ to: developerAddress, amount: developerFee, }); } if (isWhitelisted && !whitelist) { throw new Error("Whitelisted addresses not found"); } await zkAdmin.deploy({ adminPublicKey: sender, verificationKey: adminVerificationKey, whitelist: whitelist, }); zkAdmin.account.zkappUri.set(uri); await zkToken.deploy({ symbol, src: uri, verificationKey: tokenVerificationKey, }); await zkToken.initialize(adminContractAddress, decimals, // We can set `startPaused` to `Bool(false)` here, because we are doing an atomic deployment // If you are not deploying the admin and token contracts in the same transaction, // it is safer to start the tokens paused, and resume them only after verifying that // the admin contract has been deployed Bool(false)); }); return { tx, isWhitelisted, adminVerificationKey: { hash: Field(adminVerificationKey.hash), data: adminVerificationKey.data, }, tokenVerificationKey: { hash: Field(tokenVerificationKey.hash), data: tokenVerificationKey.data, }, whitelist: whitelist?.toString(), }; } export function getTokenTransactionSender(params) { const { txType, from, to } = params; if (txType === "buy" || txType === "withdrawOffer" || txType === "withdrawBid") { return to; } return from; } export async function buildTokenTransaction(params) { const { txType, chain, fee, nonce, tokenAddress, from, to, amount, price, developerAddress, developerFee, provingKey, provingFee, } = params; console.log(txType, "tx for", tokenAddress.toBase58()); const sender = getTokenTransactionSender({ txType, from, to }); console.log("Sender:", sender.toBase58()); await fetchMinaAccount({ publicKey: sender, force: true, }); if (!Mina.hasAccount(sender)) { console.error("Sender does not have account"); throw new Error("Sender does not have account"); } const { symbol, adminContractAddress, adminAddress, isWhitelisted } = await getTokenSymbolAndAdmin({ tokenAddress, chain, }); const memo = params.memo ?? `${txType} ${symbol}`; const whitelistedAdminContract = new FungibleTokenWhitelistedAdmin(adminContractAddress); const tokenContract = isWhitelisted && txType === "mint" ? WhitelistedFungibleToken : FungibleToken; if ((txType === "whitelistAdmin" || txType === "whitelistBid" || txType === "whitelistOffer") && !params.whitelist) { throw new Error("Whitelist is required"); } const whitelist = params.whitelist ? typeof params.whitelist === "string" ? Whitelist.fromString(params.whitelist) : await Whitelist.create({ list: params.whitelist, name: symbol }) : undefined; const zkToken = new tokenContract(tokenAddress); const tokenId = zkToken.deriveTokenId(); if (txType === "mint" && adminAddress.toBase58() !== sender.toBase58()) throw new Error("Invalid sender for mint"); await fetchMinaAccount({ publicKey: tokenAddress, tokenId, force: true, }); await fetchMinaAccount({ publicKey: from, tokenId, force: [ "offer", "whitelistOffer", "bid", "whitelistBid", "sell", "transfer", "withdrawOffer", ].includes(txType), }); await fetchMinaAccount({ publicKey: to, tokenId, force: [ "sell", "whitelistAdmin", "withdrawBid", "withdrawOffer", ].includes(txType), }); const isNewAccount = Mina.hasAccount(to, tokenId) === false; const offerContract = new FungibleTokenOfferContract([ "offer", "whitelistOffer", ].includes(txType) ? to : from, tokenId); const bidContract = new FungibleTokenBidContract([ "bid", "whitelistBid", ].includes(txType) ? from : to, tokenId); const offerContractDeployment = new FungibleTokenOfferContract(to, tokenId); const bidContractDeployment = new FungibleTokenBidContract(from, tokenId); const vk = tokenVerificationKeys[chain === "mainnet" ? "mainnet" : "testnet"].vk; if (!vk || !vk.FungibleTokenOfferContract || !vk.FungibleTokenOfferContract.hash || !vk.FungibleTokenOfferContract.data || !vk.FungibleTokenBidContract || !vk.FungibleTokenBidContract.hash || !vk.FungibleTokenBidContract.data || !vk.FungibleTokenWhitelistedAdmin || !vk.FungibleTokenWhitelistedAdmin.hash || !vk.FungibleTokenWhitelistedAdmin.data || !vk.FungibleTokenAdmin || !vk.FungibleTokenAdmin.hash || !vk.FungibleTokenAdmin.data || !vk.WhitelistedFungibleToken || !vk.WhitelistedFungibleToken.hash || !vk.WhitelistedFungibleToken.data || !vk.FungibleToken || !vk.FungibleToken.hash || !vk.FungibleToken.data) throw new Error("Cannot get verification key"); const adminVerificationKey = isWhitelisted ? vk.FungibleTokenWhitelistedAdmin : vk.FungibleTokenAdmin; const tokenVerificationKey = isWhitelisted ? vk.WhitelistedFungibleToken : vk.FungibleToken; const offerVerificationKey = FungibleTokenOfferContract._verificationKey ?? { hash: Field(vk.FungibleTokenOfferContract.hash), data: vk.FungibleTokenOfferContract.data, }; const bidVerificationKey = FungibleTokenBidContract._verificationKey ?? { hash: Field(vk.FungibleTokenBidContract.hash), data: vk.FungibleTokenBidContract.data, }; console.log("Sender balance:", await accountBalanceMina(sender)); console.log("New account:", isNewAccount); const tx = await Mina.transaction({ sender, fee, memo, nonce }, async () => { const feeAccountUpdate = AccountUpdate.createSigned(sender); if (isNewAccount) { feeAccountUpdate.balance.subInPlace(1_000_000_000); } feeAccountUpdate.send({ to: provingKey, amount: provingFee, }); if (developerAddress && developerFee) { feeAccountUpdate.send({ to: developerAddress, amount: developerFee, }); } switch (txType) { case "mint": if (amount === undefined) throw new Error("Error: Amount is required"); await zkToken.mint(to, amount); break; case "transfer": if (amount === undefined) throw new Error("Error: Amount is required"); await zkToken.transfer(from, to, amount); break; case "offer": if (price === undefined) throw new Error("Error: Price is required"); if (amount === undefined) throw new Error("Error: Amount is required"); if (isNewAccount) { await offerContractDeployment.deploy({ verificationKey: offerVerificationKey, whitelist: whitelist ?? Whitelist.empty(), }); offerContract.account.zkappUri.set(`Offer for ${symbol}`); await offerContract.initialize(sender, tokenAddress, amount, price); await zkToken.approveAccountUpdates([ offerContractDeployment.self, offerContract.self, ]); } else { await offerContract.offer(amount, price); await zkToken.approveAccountUpdate(offerContract.self); } break; case "buy": if (amount === undefined) throw new Error("Error: Amount is required"); await offerContract.buy(amount); await zkToken.approveAccountUpdate(offerContract.self); break; case "withdrawOffer": if (amount === undefined) throw new Error("Error: Amount is required"); await offerContract.withdraw(amount); await zkToken.approveAccountUpdate(offerContract.self); break; case "bid": if (price === undefined) throw new Error("Error: Price is required"); if (amount === undefined) throw new Error("Error: Amount is required"); if (isNewAccount) { await bidContractDeployment.deploy({ verificationKey: bidVerificationKey, whitelist: whitelist ?? Whitelist.empty(), }); bidContract.account.zkappUri.set(`Bid for ${symbol}`); await bidContract.initialize(tokenAddress, amount, price); await zkToken.approveAccountUpdates([ bidContractDeployment.self, bidContract.self, ]); } else { await bidContract.bid(amount, price); await zkToken.approveAccountUpdate(bidContract.self); } break; case "sell": if (amount === undefined) throw new Error("Error: Amount is required"); await bidContract.sell(amount); await zkToken.approveAccountUpdate(bidContract.self); break; case "withdrawBid": if (amount === undefined) throw new Error("Error: Amount is required"); await bidContract.withdraw(amount); await zkToken.approveAccountUpdate(bidContract.self); break; case "whitelistAdmin": if (!whitelist) throw new Error("Whitelist is required"); await whitelistedAdminContract.updateWhitelist(whitelist); break; case "whitelistBid": if (!whitelist) throw new Error("Whitelist is required"); await bidContract.updateWhitelist(whitelist); break; case "whitelistOffer": if (!whitelist) throw new Error("Whitelist is required"); await offerContract.updateWhitelist(whitelist); await zkToken.approveAccountUpdate(offerContract.self); break; default: throw new Error(`Unknown transaction type: ${txType}`); } }); return { tx, isWhitelisted, adminContractAddress, adminAddress, symbol, adminVerificationKey: { hash: Field(adminVerificationKey.hash), data: adminVerificationKey.data, }, tokenVerificationKey: { hash: Field(tokenVerificationKey.hash), data: tokenVerificationKey.data, }, offerVerificationKey, bidVerificationKey, whitelist: whitelist?.toString(), }; } export async function getTokenSymbolAndAdmin(params) { const { tokenAddress, chain } = params; const vk = tokenVerificationKeys[chain === "mainnet" ? "mainnet" : "testnet"].vk; class FungibleTokenState extends Struct({ decimals: UInt8, admin: PublicKey, paused: Bool, }) { } const FungibleTokenStateSize = FungibleTokenState.sizeInFields(); class FungibleTokenAdminState extends Struct({ adminPublicKey: PublicKey, }) { } const FungibleTokenAdminStateSize = FungibleTokenAdminState.sizeInFields(); await fetchMinaAccount({ publicKey: tokenAddress, force: true }); if (!Mina.hasAccount(tokenAddress)) { throw new Error("Token contract account not found"); } const account = Mina.getAccount(tokenAddress); const verificationKey = account.zkapp?.verificationKey; if (!verificationKey) { throw new Error("Token contract verification key not found"); } if (verificationKey.hash.toJSON() !== vk.FungibleToken.hash || verificationKey.data !== vk.FungibleToken.data || verificationKey.hash.toJSON() !== vk.WhitelistedFungibleToken.hash || verificationKey.data !== vk.WhitelistedFungibleToken.data) { throw new Error("Unknown token verification key"); } if (account.zkapp?.appState === undefined) { throw new Error("Token contract state not found"); } const state = FungibleTokenState.fromFields(account.zkapp?.appState.slice(0, FungibleTokenStateSize)); const symbol = account.tokenSymbol; const adminContractPublicKey = state.admin; await fetchMinaAccount({ publicKey: adminContractPublicKey, force: true, }); if (!Mina.hasAccount(adminContractPublicKey)) { throw new Error("Admin contract account not found"); } const adminContract = Mina.getAccount(adminContractPublicKey); const adminVerificationKey = adminContract.zkapp?.verificationKey; if (!adminVerificationKey) { throw new Error("Admin verification key not found"); } let isWhitelisted = false; if (vk.FungibleTokenWhitelistedAdmin.hash === adminVerificationKey.hash.toJSON() && vk.FungibleTokenWhitelistedAdmin.data === adminVerificationKey.data) { isWhitelisted = true; } else if (vk.FungibleTokenAdmin.hash === adminVerificationKey.hash.toJSON() && vk.FungibleTokenAdmin.data === adminVerificationKey.data) { isWhitelisted = false; } else { throw new Error("Unknown admin verification key"); } const adminAddress0 = adminContract.zkapp?.appState[0]; const adminAddress1 = adminContract.zkapp?.appState[1]; if (adminAddress0 === undefined || adminAddress1 === undefined) { throw new Error("Cannot fetch admin address from admin contract"); } const adminAddress = PublicKey.fromFields([adminAddress0, adminAddress1]); return { adminContractAddress: adminContractPublicKey, adminAddress: adminAddress, symbol, isWhitelisted, }; } //# sourceMappingURL=build.js.map