UNPKG

o1js

Version:

TypeScript framework for zk-SNARKs and zkApps

1,046 lines (941 loc) 37.2 kB
import { AccountId, AccountTiming } from './account.js'; import { AccountUpdateAuthorization, AccountUpdateAuthorizationEnvironment, AccountUpdateAuthorizationKind, AccountUpdateAuthorizationKindIdentifier, AccountUpdateAuthorizationKindWithZkappContext, } from './authorization.js'; import { Option, TokenId, Update, ZkappUri, mapUndefined } from './core.js'; import { Permissions, PermissionsDescription } from './permissions.js'; import { Preconditions, PreconditionsDescription } from './preconditions.js'; import { GenericStateUpdates, StateDefinition, StateLayout, StateUpdates } from './state.js'; import { Pickles } from '../../../bindings.js'; import { Bool } from '../../provable/bool.js'; import { Field } from '../../provable/field.js'; import { Int64, UInt64 } from '../../provable/int.js'; import { Proof } from '../../proof-system/zkprogram.js'; import { emptyHashWithPrefix, hashWithPrefix, packToFields, TokenSymbol, } from '../../provable/crypto/poseidon.js'; import { PublicKey } from '../../provable/crypto/signature.js'; import { HashInput } from '../../provable/types/provable-derivers.js'; import { Provable } from '../../provable/types/provable-intf.js'; import { mocks, prefixes } from '../../../bindings/crypto/constants.js'; import * as Bindings from '../../../bindings/mina-transaction/v2/index.js'; import * as PoseidonBigint from '../../../mina-signer/src/poseidon-bigint.js'; import { Signature, signFieldElement, zkAppBodyPrefix, } from '../../../mina-signer/src/signature.js'; import { NetworkId } from '../../../mina-signer/src/types.js'; import { Struct } from '../../provable/types/struct.js'; import { VerificationKey } from '../../../lib/proof-system/verification-key.js'; // TODO: make private abstractions over many fields (eg new apis for Update and Constraint.*) // TODO: replay checks export { AccountUpdate, Authorized, GenericData, AccountUpdateTree, AccountUpdateTreeDescription, ContextFreeAccountUpdateDescription, ContextFreeAccountUpdate, DynamicProvable, AccountUpdateCommitment, CommittedList, EventsHashConfig, ActionsHashConfig, }; class AccountUpdateCommitment extends Struct({ accountUpdateCommitment: Field, }) { constructor(accountUpdateCommitment: Field) { super({ accountUpdateCommitment }); } } // TODO: move elsewhere type DynamicProvable<T> = | Provable<T> | { toFields(x: T): Field[]; toAuxiliary(x: T): any[]; fromFieldsDynamic(fields: Field[], aux: any[]): { value: T; fieldsConsumed: number }; }; // TODO: move elsewhere const GenericData: DynamicProvable<Field[]> = { toFields(x: Field[]): Field[] { return x; }, toAuxiliary(x: Field[]): any[] { return [x.length]; }, fromFieldsDynamic(fields: Field[], aux: any[]): { value: Field[]; fieldsConsumed: number } { const [_len] = aux; let len = _len ?? fields.length; return { value: fields.slice(0, len), fieldsConsumed: len }; }, }; // TODO: move elsewhere /* interface Hashable { hash(): Field; } */ // TODO: move elsewhere interface HashableDataConfig<Item> { readonly emptyPrefix: string; readonly consPrefix: string; hash(item: Item): Field; } function EventsHashConfig<T>(T: DynamicProvable<T>): HashableDataConfig<T> { return { emptyPrefix: 'MinaZkappEventsEmpty', consPrefix: prefixes.events, hash(x: T): Field { const fields = T.toFields(x); return hashWithPrefix(prefixes.event, fields); }, }; } function ActionsHashConfig<T>(T: DynamicProvable<T>): HashableDataConfig<T> { return { emptyPrefix: 'MinaZkappActionsEmpty', consPrefix: prefixes.sequenceEvents, hash(x: T): Field { const fields = T.toFields(x); return hashWithPrefix(prefixes.event, fields); }, }; } // TODO: move elsewhere class CommittedList<Item> { readonly Item: DynamicProvable<Item>; readonly data: Item[]; readonly hash: Field; constructor({ Item, data, hash }: { Item: DynamicProvable<Item>; data: Item[]; hash: Field }) { this.Item = Item; this.data = data; this.hash = hash; } toInternalRepr(): { data: Field[][]; hash: Field } { return { data: this.data.map(this.Item.toFields), hash: this.hash, }; } // IMPORTANT: It is the callers responsibility to ensure the commitment will compute the same // after mapping the list (this function does not check this for you at runtime). mapUnsafe<B>(NewItem: DynamicProvable<B>, f: (a: Item) => B): CommittedList<B> { return new CommittedList({ Item: NewItem, data: this.data.map(f), hash: this.hash, }); } static hashList<Item>(config: HashableDataConfig<Item>, items: Item[]): Field { let hash = emptyHashWithPrefix(config.emptyPrefix); for (let i = items.length - 1; i >= 0; i--) { const item = items[i]; hash = hashWithPrefix(config.consPrefix, [hash, config.hash(item)]); } return hash; } static from<Item>( Item: DynamicProvable<Item>, config: HashableDataConfig<Item>, value: undefined | Item[] | CommittedList<Item> | Bindings.Leaves.CommittedList ): CommittedList<Item> { if (value instanceof CommittedList) return value; let items: Item[]; //let hash; if (value === undefined) { items = []; } else if (value instanceof Array) { items = value; } else { // TODO: think about this a bit more... we don't have the aux data here, so we should do // something to restrict the types if ('fromFields' in Item) { items = value.data.map((fields) => Item.fromFields(fields, [])); } else { items = value.data.map((fields) => { const { value: result, fieldsConsumed } = Item.fromFieldsDynamic(fields, []); if (fieldsConsumed !== fields.length) throw new Error('expected all fields to be consumed when casting dynamic item'); return result; }); } //hash = value.hash; } //hash = hash ?? CommittedList.hashList(config, items); return new CommittedList({ Item, data: items, hash: CommittedList.hashList(config, items), }); } } interface MayUseToken { parentsOwnToken: Bool; inheritFromParent: Bool; } type AccountUpdateTreeDescription<RootDescription, Child> = RootDescription & { // TODO: support using commitments? children?: AccountUpdateTree<Child>[]; }; // TODO: CONSIDER -- merge this logic into AccountUpdate // class AccountUpdateTree<AccountUpdateType> { class AccountUpdateTree<Root, Child = Root> { constructor( public rootAccountUpdate: Root, public children: AccountUpdateTree<Child, Child>[] ) {} // depth first traversal (parents before children) static forEachNode<T>( tree: AccountUpdateTree<T>, depth: number, f: (accountUpdate: T, depth: number) => void ): void { f(tree.rootAccountUpdate, depth); tree.children.forEach((child) => AccountUpdateTree.forEachNode(child, depth + 1, f)); } // inverted depth first traversal (children before parents) static forEachNodeInverted<T>( tree: AccountUpdateTree<T>, depth: number, f: (accountUpdate: T, depth: number) => void ): void { tree.children.forEach((child) => AccountUpdateTree.forEachNodeInverted(child, depth + 1, f)); f(tree.rootAccountUpdate, depth); } static reduce<T, R>(tree: AccountUpdateTree<T>, f: (accountUpdate: T, childValues: R[]) => R): R { const childValues = tree.children.map((child) => AccountUpdateTree.reduce(child, f)); return f(tree.rootAccountUpdate, childValues); } // TODO: delete (realized I didn't need it part way through writing) // // build context as we descend the tree, map, then reduce as we ascend // static mapReduceWithContext<T, Ctx, R>( // tree: AccountUpdateTree<T>, // context: Ctx, // mapContext: (ctx: Ctx, index: number) => Ctx, // map: (ctx: Ctx, accountUpdate: T) => R, // reduce: (values: R[]) => R // ): R { // if(tree.children.length === 0) { // return map(context, tree.rootAccountUpdate); // } else { // const reducedValues = tree.children.map((child, index) => // AccountUpdateTree.mapReduceWithContext( // child, // mapContext(context, index), // mapContext, // map, // reduce // ); // ); // return reduce(reducedValues); // } // } // TODO: refactor the type parameter interfaces static mapRoot<RootIn, RootOut, Child>( tree: AccountUpdateTree<RootIn, Child>, f: (accountUpdate: RootIn) => RootOut ): AccountUpdateTree<RootOut, Child> { const newAccountUpdate = f(tree.rootAccountUpdate); return new AccountUpdateTree(newAccountUpdate, tree.children); } static async map<A, B>( tree: AccountUpdateTree<A>, f: (accountUpdate: A) => Promise<B> ): Promise<AccountUpdateTree<B>> { const newAccountUpdate = await f(tree.rootAccountUpdate); const newChildren = await AccountUpdateTree.mapForest(tree.children, f); return new AccountUpdateTree(newAccountUpdate, newChildren); } static mapForest<A, B>( forest: AccountUpdateTree<A>[], f: (a: A) => Promise<B> ): Promise<AccountUpdateTree<B>[]> { return Promise.all(forest.map((tree) => AccountUpdateTree.map(tree, f))); } // TODO: I think this can be safely made polymorphic over the account update state, actions, and events representations // TODO: Field, not bigint static hash(tree: AccountUpdateTree<AccountUpdate>, networkId: NetworkId): bigint { // TODO: is it ok to do this and ignore the toValue encodings entirely? const accountUpdateFieldInput = tree.rootAccountUpdate.toInput(); const accountUpdateBigintInput = { fields: accountUpdateFieldInput.fields?.map((f: Field) => f.toBigInt()), packed: accountUpdateFieldInput.packed?.map(([f, n]: [Field, number]): [bigint, number] => [ f.toBigInt(), n, ]), }; // TODO: negotiate between this implementation and AccountUpdate#hash to figure out what is correct // TODO NOW: ^ this was done, but we need to update this function to share code still const accountUpdateCommitment = PoseidonBigint.hashWithPrefix( zkAppBodyPrefix(networkId), PoseidonBigint.packToFields(accountUpdateBigintInput) ); const childrenCommitment = AccountUpdateTree.hashForest(networkId, tree.children); return PoseidonBigint.hashWithPrefix(prefixes.accountUpdateNode, [ accountUpdateCommitment, childrenCommitment, ]); } // TODO: Field, not bigint static hashForest(networkId: NetworkId, forest: AccountUpdateTree<AccountUpdate>[]): bigint { const consHash = (acc: bigint, tree: AccountUpdateTree<AccountUpdate>) => PoseidonBigint.hashWithPrefix(prefixes.accountUpdateCons, [ AccountUpdateTree.hash(tree, networkId), acc, ]); return [...forest].reverse().reduce(consHash, 0n); } static unrollForest<AccountUpdateType, Return>( forest: AccountUpdateTree<AccountUpdateType>[], f: (accountUpdate: AccountUpdateType, depth: number) => Return ): Return[] { const seq: Return[] = []; forest.forEach((tree) => AccountUpdateTree.forEachNode(tree, 0, (accountUpdate, depth) => seq.push(f(accountUpdate, depth)) ) ); return seq; } static sizeInFields(): number { return AccountUpdate.sizeInFields(); } static toFields(x: AccountUpdateTree<AccountUpdate>): Field[] { return AccountUpdate.toFields(x.rootAccountUpdate); } static toAuxiliary(x?: AccountUpdateTree<AccountUpdate>): any[] { return [AccountUpdate.toAuxiliary(x?.rootAccountUpdate), x?.children ?? []]; } static fromFields(fields: Field[], aux: any[]): AccountUpdateTree<AccountUpdate> { return new AccountUpdateTree(AccountUpdate.fromFields(fields, aux[0]), aux[1]); } static toValue(x: AccountUpdateTree<AccountUpdate>): AccountUpdateTree<AccountUpdate> { return x; } static fromValue(x: AccountUpdateTree<AccountUpdate>): AccountUpdateTree<AccountUpdate> { return x; } static check(_x: AccountUpdateTree<AccountUpdate>): void { // TODO } static from<RootDescription, Root, Child>( descr: AccountUpdateTreeDescription<RootDescription, Child>, createAccountUpdate: (descr: RootDescription) => Root ): AccountUpdateTree<Root, Child> { return new AccountUpdateTree(createAccountUpdate(descr), descr.children ?? []); } } interface ContextFreeAccountUpdateDescription< State extends StateLayout = 'GenericState', Event = Field[], Action = Field[], > { // TODO: accept identifiers for authorization kind authorizationKind: AccountUpdateAuthorizationKindIdentifier | AccountUpdateAuthorizationKind; preconditions?: PreconditionsDescription<State> | Preconditions<State>; balanceChange?: Int64; incrementNonce?: Bool; useFullCommitment?: Bool; implicitAccountCreationFee?: Bool; mayUseToken?: MayUseToken; pushEvents?: Event[] | CommittedList<Event>; pushActions?: Action[] | CommittedList<Action>; setState?: StateUpdates<State>; setPermissions?: PermissionsDescription | Permissions | Update<Permissions>; setDelegate?: PublicKey | Update<PublicKey>; setVerificationKey?: VerificationKey | Update<VerificationKey>; setZkappUri?: string | ZkappUri | Update<ZkappUri>; setTokenSymbol?: string | TokenSymbol | Update<TokenSymbol>; setTiming?: AccountTiming | Update<AccountTiming>; setVotingFor?: Field | Update<Field>; } // in a ZkModule context: ContextFreeAccountUpdate is an AccountUpdate without an account id and call data class ContextFreeAccountUpdate< State extends StateLayout = 'GenericState', Event = Field[], Action = Field[], > { readonly State: StateDefinition<State>; authorizationKind: AccountUpdateAuthorizationKind; preconditions: Preconditions<State>; balanceChange: Int64; incrementNonce: Bool; useFullCommitment: Bool; implicitAccountCreationFee: Bool; mayUseToken: MayUseToken; pushEvents: CommittedList<Event>; pushActions: CommittedList<Action>; // TODO: standardize on these being set* for *Update, don't do both stateUpdates: StateUpdates<State>; permissionsUpdate: Update<Permissions>; delegateUpdate: Update<PublicKey>; verificationKeyUpdate: Update<VerificationKey>; zkappUriUpdate: Update<ZkappUri>; tokenSymbolUpdate: Update<TokenSymbol>; timingUpdate: Update<AccountTiming>; votingForUpdate: Update<Field>; constructor( State: StateDefinition<State>, Event: DynamicProvable<Event>, Action: DynamicProvable<Action>, descr: | ContextFreeAccountUpdateDescription<State, Event, Action> | ContextFreeAccountUpdate<State, Event, Action> ) { function castUpdate<A, B>( value: undefined | A | Update<B>, defaultValue: B, f: (a: A) => B ): Update<B> { if (value instanceof Update) { return value; } else { return Update.from(mapUndefined(value, f), defaultValue); } } this.State = State; this.authorizationKind = AccountUpdateAuthorizationKind.from(descr.authorizationKind); this.preconditions = mapUndefined(descr.preconditions, (x) => Preconditions.from(State, x)) ?? Preconditions.emptyPoly(State); this.balanceChange = descr.balanceChange ?? Int64.create(UInt64.zero); this.incrementNonce = descr.incrementNonce ?? new Bool(false); this.useFullCommitment = descr.useFullCommitment ?? new Bool(false); this.implicitAccountCreationFee = descr.implicitAccountCreationFee ?? new Bool(false); this.mayUseToken = descr.mayUseToken ?? { parentsOwnToken: new Bool(false), inheritFromParent: new Bool(false), }; this.pushEvents = CommittedList.from(Event, EventsHashConfig(Event), descr.pushEvents); this.pushActions = CommittedList.from(Action, ActionsHashConfig(Action), descr.pushActions); if (descr instanceof ContextFreeAccountUpdate) { this.stateUpdates = descr.stateUpdates; this.permissionsUpdate = descr.permissionsUpdate; this.delegateUpdate = descr.delegateUpdate; this.verificationKeyUpdate = descr.verificationKeyUpdate; this.zkappUriUpdate = descr.zkappUriUpdate; this.tokenSymbolUpdate = descr.tokenSymbolUpdate; this.timingUpdate = descr.timingUpdate; this.votingForUpdate = descr.votingForUpdate; } else { this.stateUpdates = descr.setState ?? StateUpdates.empty(State); this.permissionsUpdate = castUpdate( descr.setPermissions, Permissions.empty(), Permissions.from ); this.delegateUpdate = Update.from(descr.setDelegate, PublicKey.empty()); this.verificationKeyUpdate = Update.from(descr.setVerificationKey, VerificationKey.empty()); this.zkappUriUpdate = castUpdate(descr.setZkappUri, ZkappUri.empty(), ZkappUri.from); this.tokenSymbolUpdate = castUpdate( descr.setTokenSymbol, TokenSymbol.empty(), TokenSymbol.from ); this.timingUpdate = Update.from(descr.setTiming, AccountTiming.empty()); this.votingForUpdate = Update.from(descr.setVotingFor, Field.empty()); } } toGeneric(): ContextFreeAccountUpdate { return ContextFreeAccountUpdate.generic({ authorizationKind: this.authorizationKind, preconditions: this.preconditions.toGeneric(), balanceChange: this.balanceChange, incrementNonce: this.incrementNonce, useFullCommitment: this.useFullCommitment, implicitAccountCreationFee: this.implicitAccountCreationFee, mayUseToken: this.mayUseToken, pushEvents: this.pushEvents.mapUnsafe(GenericData, this.pushEvents.Item.toFields), pushActions: this.pushActions.mapUnsafe(GenericData, this.pushActions.Item.toFields), setState: StateUpdates.toGeneric(this.State, this.stateUpdates), setPermissions: this.permissionsUpdate, setDelegate: this.delegateUpdate, setVerificationKey: this.verificationKeyUpdate, setZkappUri: this.zkappUriUpdate, setTokenSymbol: this.tokenSymbolUpdate, setTiming: this.timingUpdate, setVotingFor: this.votingForUpdate, }); } static fromGeneric<State extends StateLayout, Event, Action>( x: ContextFreeAccountUpdate, State: StateDefinition<State>, Event: DynamicProvable<Event>, Action: DynamicProvable<Action> ): ContextFreeAccountUpdate<State, Event, Action> { // TODO: this method is broken because we aren't storing aux data in the generic format... return new ContextFreeAccountUpdate(State, Event, Action, { authorizationKind: x.authorizationKind, preconditions: Preconditions.fromGeneric(x.preconditions, State), balanceChange: x.balanceChange, incrementNonce: x.incrementNonce, useFullCommitment: x.useFullCommitment, implicitAccountCreationFee: x.implicitAccountCreationFee, mayUseToken: x.mayUseToken, pushEvents: x.pushEvents.mapUnsafe(Event, (fields) => // TODO: this is really unsafe, make it safe 'fromFieldsDynamic' in Event ? Event.fromFieldsDynamic(fields, []).value : Event.fromFields(fields, []) ), pushActions: x.pushActions.mapUnsafe(Action, (fields) => // TODO: this is really unsafe, make it safe 'fromFieldsDynamic' in Action ? Action.fromFieldsDynamic(fields, []).value : Action.fromFields(fields, []) ), setState: StateUpdates.fromGeneric(x.stateUpdates, State), setPermissions: x.permissionsUpdate, setDelegate: x.delegateUpdate, setVerificationKey: x.verificationKeyUpdate, setZkappUri: x.zkappUriUpdate, setTokenSymbol: x.tokenSymbolUpdate, setTiming: x.timingUpdate, setVotingFor: x.votingForUpdate, }); } static generic(descr: ContextFreeAccountUpdateDescription): ContextFreeAccountUpdate { return new ContextFreeAccountUpdate('GenericState', GenericData, GenericData, descr); } static emptyPoly<State extends StateLayout, Event, Action>( State: StateDefinition<State>, Event: DynamicProvable<Event>, Action: DynamicProvable<Action> ) { return new ContextFreeAccountUpdate(State, Event, Action, { authorizationKind: AccountUpdateAuthorizationKind.None(), }); } static empty(): ContextFreeAccountUpdate { return ContextFreeAccountUpdate.emptyPoly('GenericState', GenericData, GenericData); } static from<State extends StateLayout = 'GenericState', Event = Field[], Action = Field[]>( State: StateDefinition<State>, Event: DynamicProvable<Event>, Action: DynamicProvable<Action>, x: | ContextFreeAccountUpdateDescription<State, Event, Action> | ContextFreeAccountUpdate<State, Event, Action> | undefined ): ContextFreeAccountUpdate<State, Event, Action> { if (x instanceof ContextFreeAccountUpdate) return x; if (x === undefined) return ContextFreeAccountUpdate.emptyPoly(State, Event, Action); return new ContextFreeAccountUpdate(State, Event, Action, x); } } type AccountUpdateDescription<State extends StateLayout, Event = Field[], Action = Field[]> = ( | { update: ContextFreeAccountUpdate<State, Event, Action>; proof?: Proof<undefined, AccountUpdateCommitment>; } | ContextFreeAccountUpdateDescription<State, Event, Action> ) & { accountId: AccountId; verificationKeyHash?: Field; callData?: Field; }; class AccountUpdate< State extends StateLayout = 'GenericState', Event = Field[], Action = Field[], > extends ContextFreeAccountUpdate<State, Event, Action> { accountId: AccountId; verificationKeyHash: Field; callData: Field; // TODO: this probable shouldn't live here, but somewhere else proof: 'NoProofRequired' | 'ProofPending' | Proof<undefined, AccountUpdateCommitment>; // TODO: circuit friendly representation (we really don't want to toBoolean() in the constructor here...) // proof: {pending: true, isRequired: Bool} | Proof<undefined, AccountUpdateCommitment>; constructor( State: StateDefinition<State>, Event: DynamicProvable<Event>, Action: DynamicProvable<Action>, descr: AccountUpdateDescription<State, Event, Action> ) { // TODO NOW: THIS PATTERN IS BROKEN (we are casting and update into a description, which only works because the missing fields are all optional) const superInput = 'update' in descr ? descr.update : descr; super(State, Event, Action, superInput); if (this.authorizationKind.isProved.toBoolean()) { this.proof = 'proof' in descr && descr.proof !== undefined ? descr.proof : 'ProofPending'; } else { if ('proof' in descr && descr.proof !== undefined) { throw new Error( 'proof was provided when constructing an AccountUpdate that does not require a proof' ); } this.proof = 'NoProofRequired'; } this.accountId = descr.accountId; this.verificationKeyHash = descr.verificationKeyHash ?? new Field(mocks.dummyVerificationKeyHash); this.callData = descr.callData ?? new Field(0); } get authorizationKindWithZkappContext(): AccountUpdateAuthorizationKindWithZkappContext { return new AccountUpdateAuthorizationKindWithZkappContext( this.authorizationKind, this.verificationKeyHash ); } toInternalRepr(callDepth: number): Bindings.Layout.AccountUpdateBody { return { authorizationKind: this.authorizationKindWithZkappContext, publicKey: this.accountId.publicKey, tokenId: this.accountId.tokenId.value, callData: this.callData, callDepth: callDepth, balanceChange: this.balanceChange, incrementNonce: this.incrementNonce, useFullCommitment: this.useFullCommitment, implicitAccountCreationFee: this.implicitAccountCreationFee, mayUseToken: this.mayUseToken, events: this.pushEvents.toInternalRepr(), actions: this.pushActions.toInternalRepr(), preconditions: this.preconditions.toInternalRepr(), update: { appState: StateUpdates.toFieldUpdates(this.State, this.stateUpdates).map((update) => update.toOption() ), delegate: this.delegateUpdate.toOption(), verificationKey: Option.map(this.verificationKeyUpdate.toOption(), (data) => data instanceof VerificationKey ? new VerificationKey(data) : data ), permissions: this.permissionsUpdate.toOption(), zkappUri: this.zkappUriUpdate.toOption(), tokenSymbol: this.tokenSymbolUpdate.toOption(), timing: this.timingUpdate.toOption(), votingFor: this.votingForUpdate.toOption(), }, }; } toInput(): HashInput { return Bindings.Layout.AccountUpdateBody.toInput(this.toInternalRepr(0)); } commit(networkId: NetworkId): AccountUpdateCommitment { const commitment = hashWithPrefix(zkAppBodyPrefix(networkId), packToFields(this.toInput())); return new AccountUpdateCommitment(commitment); } toGeneric(): AccountUpdate { return new AccountUpdate('GenericState', GenericData, GenericData, { update: super.toGeneric(), proof: this.proof instanceof Proof ? this.proof : undefined, accountId: this.accountId, verificationKeyHash: this.verificationKeyHash, callData: this.callData, }); } static fromGeneric<State extends StateLayout, Event, Action>( x: AccountUpdate, State: StateDefinition<State>, Event: DynamicProvable<Event>, Action: DynamicProvable<Action> ): AccountUpdate<State, Event, Action> { return new AccountUpdate(State, Event, Action, { update: ContextFreeAccountUpdate.fromGeneric(x, State, Event, Action), proof: x.proof instanceof Proof ? x.proof : undefined, accountId: x.accountId, verificationKeyHash: x.verificationKeyHash, callData: x.callData, }); } async authorize( authEnv: AccountUpdateAuthorizationEnvironment ): Promise<Authorized<State, Event, Action>> { let proof = null; let signature = null; switch (this.proof) { case 'NoProofRequired': if (this.authorizationKind.isProved.toBoolean()) { throw new Error( `account update proof was marked as not required, but authorization kind was ${this.authorizationKind.identifier()}` ); } else { break; } case 'ProofPending': if (this.authorizationKind.isProved.toBoolean()) { throw new Error( `account update proof is still pending; a proof must be generated and assigned to an account update before calling authorize` ); } else { console.warn( `account update is marked to required a proof, but the authorization kind is ${this.authorizationKind.identifier()} (and the proof is still pending)` ); break; } default: if (this.authorizationKind.isProved.toBoolean()) { proof = Pickles.proofToBase64Transaction(this.proof.proof); } else { console.warn( `account update has a proof, but no proof is required by authorization kind ${this.authorizationKind.identifier()}, so it will not be included` ); } } if (this.authorizationKind.isSigned.toBoolean()) { let txnCommitment; if (this.useFullCommitment.toBoolean()) { if (authEnv.fullTransactionCommitment === undefined) { throw new Error( 'unable to authorize account update: useFullCommitment is true, but not full transaction commitment was provided in authorization environment' ); } txnCommitment = authEnv.fullTransactionCommitment; } else { txnCommitment = authEnv.accountUpdateForestCommitment; } const privateKey = await authEnv.getPrivateKey(this.accountId.publicKey); const sig = signFieldElement(txnCommitment, privateKey.toBigInt(), authEnv.networkId); signature = Signature.toBase58(sig); } return new Authorized({ proof, signature }, this); } static create(x: AccountUpdateDescription<'GenericState', Field[], Field[]>): AccountUpdate { return new AccountUpdate('GenericState', GenericData, GenericData, x); } static fromInternalRepr(x: Bindings.Layout.AccountUpdateBody): AccountUpdate { return new AccountUpdate('GenericState', GenericData, GenericData, { accountId: new AccountId(x.publicKey, new TokenId(x.tokenId)), verificationKeyHash: x.authorizationKind.verificationKeyHash, authorizationKind: new AccountUpdateAuthorizationKind(x.authorizationKind), callData: x.callData, balanceChange: Int64.create(x.balanceChange.magnitude, x.balanceChange.sgn), incrementNonce: x.incrementNonce, useFullCommitment: x.useFullCommitment, implicitAccountCreationFee: x.implicitAccountCreationFee, mayUseToken: x.mayUseToken, pushEvents: CommittedList.from(GenericData, EventsHashConfig(GenericData), x.events), pushActions: CommittedList.from(GenericData, ActionsHashConfig(GenericData), x.actions), preconditions: Preconditions.fromInternalRepr(x.preconditions), setState: new GenericStateUpdates(x.update.appState.map(Update.fromOption)), setDelegate: Update.fromOption(x.update.delegate), setVerificationKey: Update.fromOption(x.update.verificationKey), setPermissions: Update.fromOption( Option.map(x.update.permissions, Permissions.fromInternalRepr) ), setZkappUri: Update.fromOption(Option.map(x.update.zkappUri, (uri) => new ZkappUri(uri))), setTokenSymbol: Update.fromOption( Option.map(x.update.tokenSymbol, (symbol) => new TokenSymbol(symbol)) ), setTiming: Update.fromOption( Option.map(x.update.timing, (timing) => new AccountTiming(timing)) ), setVotingFor: Update.fromOption(x.update.votingFor), }); } static sizeInFields(): number { return Bindings.Layout.AccountUpdateBody.sizeInFields(); } static toFields(x: AccountUpdate): Field[] { return Bindings.Layout.AccountUpdateBody.toFields(x.toInternalRepr(0)); } static toAuxiliary(x?: AccountUpdate, callDepth?: number): any[] { return Bindings.Layout.AccountUpdateBody.toAuxiliary(x?.toInternalRepr(callDepth ?? 0)); } static fromFields(fields: Field[], aux: any[]): AccountUpdate { return AccountUpdate.fromInternalRepr( Bindings.Layout.AccountUpdateBody.fromFields(fields, aux) ); } static toValue(x: AccountUpdate): AccountUpdate { return x; } static fromValue(x: AccountUpdate): AccountUpdate { return x; } static check(_x: AccountUpdate): void { // TODO } static empty(): AccountUpdate { return new AccountUpdate('GenericState', GenericData, GenericData, { accountId: AccountId.empty(), callData: Field.empty(), update: ContextFreeAccountUpdate.empty(), }); } } // TODO NOW: un-namespace this /* type ContextFreeDescription< State extends StateLayout = 'GenericState', Event = Field[], Action = Field[] > = ContextFreeAccountUpdateDescription<State, Event, Action>; type ContextFree< State extends StateLayout = 'GenericState', Event = Field[], Action = Field[] > = ContextFreeAccountUpdate<State, Event, Action>; const ContextFree = ContextFreeAccountUpdate; */ // TODO: can we enforce that Authorized account updates are immutable? class Authorized<State extends StateLayout = 'GenericState', Event = Field[], Action = Field[]> { authorization: AccountUpdateAuthorization; private update: AccountUpdate<State, Event, Action>; constructor( authorization: AccountUpdateAuthorization, update: AccountUpdate<State, Event, Action> ) { this.authorization = authorization; this.update = update; } toAccountUpdate(): AccountUpdate<State, Event, Action> { return this.update; } get State(): StateDefinition<State> { return this.update.State; } get authorizationKind(): AccountUpdateAuthorizationKind { return this.update.authorizationKind; } get accountId(): AccountId { return this.update.accountId; } get verificationKeyHash(): Field { return this.update.verificationKeyHash; } get callData(): Field { return this.update.callData; } get preconditions(): Preconditions<State> { return this.update.preconditions; } get balanceChange(): Int64 { return this.update.balanceChange; } get incrementNonce(): Bool { return this.update.incrementNonce; } get useFullCommitment(): Bool { return this.update.useFullCommitment; } get implicitAccountCreationFee(): Bool { return this.update.implicitAccountCreationFee; } get mayUseToken(): MayUseToken { return this.update.mayUseToken; } get pushEvents(): CommittedList<Event> { return this.update.pushEvents; } get pushActions(): CommittedList<Action> { return this.update.pushActions; } get stateUpdates(): StateUpdates<State> { return this.update.stateUpdates; } get permissionsUpdate(): Update<Permissions> { return this.update.permissionsUpdate; } get delegateUpdate(): Update<PublicKey> { return this.update.delegateUpdate; } get verificationKeyUpdate(): Update<VerificationKey> { return this.update.verificationKeyUpdate; } get zkappUriUpdate(): Update<ZkappUri> { return this.update.zkappUriUpdate; } get tokenSymbolUpdate(): Update<TokenSymbol> { return this.update.tokenSymbolUpdate; } get timingUpdate(): Update<AccountTiming> { return this.update.timingUpdate; } get votingForUpdate(): Update<Field> { return this.update.votingForUpdate; } get authorizationKindWithZkappContext(): AccountUpdateAuthorizationKindWithZkappContext { return this.update.authorizationKindWithZkappContext; } hash(netId: NetworkId): Field { let input = Bindings.Layout.ZkappAccountUpdate.toInput(this.toInternalRepr(0)); return hashWithPrefix(zkAppBodyPrefix(netId), packToFields(input)); } toInternalRepr(callDepth: number): Bindings.Layout.ZkappAccountUpdate { return { authorization: { proof: this.authorization.proof === null ? undefined : this.authorization.proof, signature: this.authorization.signature === null ? undefined : this.authorization.signature, }, body: this.update.toInternalRepr(callDepth), }; } static fromInternalRepr(x: Bindings.Layout.ZkappAccountUpdate): Authorized { return new Authorized( { // when the internal representation is returned from the previous version when casting from fields, // (if there is no proof or authorization, values are set to false rather than to undefined) proof: (x.authorization.proof as any) !== false ? (x.authorization.proof ?? null) : null, signature: (x.authorization.proof as any) !== false ? (x.authorization.signature ?? null) : null, }, AccountUpdate.fromInternalRepr(x.body) ); } toJSON(callDepth: number): any { return Authorized.toJSON(this, callDepth); } toInput(): HashInput { return Authorized.toInput(this); } toFields(): Field[] { return Authorized.toFields(this); } static empty(): Authorized { return new Authorized({ proof: null, signature: null }, AccountUpdate.empty()); } static sizeInFields(): number { return Bindings.Layout.ZkappAccountUpdate.sizeInFields(); } static toJSON<State extends StateLayout = 'GenericState', Event = Field[], Action = Field[]>( x: Authorized<State, Event, Action>, callDepth: number ): any { return Bindings.Layout.ZkappAccountUpdate.toJSON(x.toInternalRepr(callDepth)); } static toInput<State extends StateLayout = 'GenericState', Event = Field[], Action = Field[]>( x: Authorized<State, Event, Action> ): HashInput { return Bindings.Layout.ZkappAccountUpdate.toInput(x.toInternalRepr(0)); } static toFields<State extends StateLayout = 'GenericState', Event = Field[], Action = Field[]>( x: Authorized<State, Event, Action> ): Field[] { return Bindings.Layout.ZkappAccountUpdate.toFields(x.toInternalRepr(0)); } static toAuxiliary<State extends StateLayout = 'GenericState', Event = Field[], Action = Field[]>( x?: Authorized<State, Event, Action>, callDepth?: number ): any[] { return Bindings.Layout.ZkappAccountUpdate.toAuxiliary(x?.toInternalRepr(callDepth ?? 0)); } static fromFields(fields: Field[], aux: any[]): Authorized { return Authorized.fromInternalRepr(Bindings.Layout.ZkappAccountUpdate.fromFields(fields, aux)); } static toValue<State extends StateLayout = 'GenericState', Event = Field[], Action = Field[]>( x: Authorized<State, Event, Action> ): Authorized<State, Event, Action> { return x; } static fromValue<State extends StateLayout = 'GenericState', Event = Field[], Action = Field[]>( x: Authorized<State, Event, Action> ): Authorized<State, Event, Action> { return x; } static check<State extends StateLayout = 'GenericState', Event = Field[], Action = Field[]>( _x: Authorized<State, Event, Action> ) { throw new Error('TODO'); } }