o1js
Version:
TypeScript framework for zk-SNARKs and zkApps
428 lines (401 loc) • 16.5 kB
JavaScript
import { Permissions } from './permissions.js';
import { StateValues } from './state.js';
import { VerificationKey } from '../../proof-system/verification-key.js';
import { Bool } from '../../provable/bool.js';
import { Field } from '../../provable/field.js';
import { UInt64, UInt32 } from '../../provable/int.js';
import { Provable } from '../../provable/provable.js';
import { PublicKey } from '../../provable/crypto/signature.js';
import { Unconstrained } from '../../provable/types/unconstrained.js';
import { TokenSymbol } from '../../../lib/provable/crypto/poseidon.js';
import { TokenId, ZkappUri } from './core.js';
export { AccountId, AccountTiming, AccountIdSet, Account, AccountIdMap };
function accountIdKeys(accountId) {
return {
publicKey: accountId.publicKey.toBase58(),
tokenId: accountId.tokenId.toString(),
};
}
class AccountId {
constructor(publicKey, tokenId) {
this.publicKey = publicKey;
this.tokenId = tokenId;
}
equals(x) {
return Bool.allTrue([this.publicKey.equals(x.publicKey), this.tokenId.equals(x.tokenId)]);
}
static empty() {
return new AccountId(PublicKey.empty(), TokenId.MINA);
}
static sizeInFields() {
return PublicKey.sizeInFields() + Field.sizeInFields();
}
static toFields(x) {
return [...PublicKey.toFields(x.publicKey), x.tokenId.value];
}
static toAuxiliary(_x) {
return [];
}
static fromFields(fields, _aux) {
return new AccountId(PublicKey.fromFields(fields.slice(0, PublicKey.sizeInFields())), new TokenId(fields[PublicKey.sizeInFields()]));
}
static toValue(x) {
return x;
}
static fromValue(x) {
return x;
}
static check(_x) {
// TODO NOW
}
}
class AccountIdMap {
constructor() {
this.data = {};
}
has(accountId) {
const { publicKey, tokenId } = accountIdKeys(accountId);
const tokenAccounts = this.data[publicKey] ?? {};
return tokenId in tokenAccounts;
}
get(accountId) {
const { publicKey, tokenId } = accountIdKeys(accountId);
const tokenAccounts = this.data[publicKey] ?? {};
return tokenAccounts[tokenId] ?? null;
}
set(accountId, value) {
const { publicKey, tokenId } = accountIdKeys(accountId);
if (!(publicKey in this.data))
this.data[publicKey] = {};
this.data[publicKey][tokenId] = value;
}
update(accountId, f) {
const value = this.get(accountId);
const updatedValue = f(value);
this.set(accountId, updatedValue);
}
}
class AccountIdSet {
constructor() {
this.idMap = new AccountIdMap();
}
has(accountId) {
return this.idMap.has(accountId);
}
add(accountId) {
this.idMap.set(accountId, null);
}
}
class AccountTiming {
constructor({ initialMinimumBalance, cliffTime, cliffAmount, vestingPeriod, vestingIncrement, }) {
this.initialMinimumBalance = initialMinimumBalance;
this.cliffTime = cliffTime;
this.cliffAmount = cliffAmount;
this.vestingPeriod = vestingPeriod;
this.vestingIncrement = vestingIncrement;
}
minimumBalanceAtSlot(globalSlot) {
// TODO: implement the provable friendly version of this function
// const beforeVestingCliff = globalSlot.lessThan(this.cliffTime);
// Provable.if(
// beforeVestingCliff,
// UInt64,
// this.initialMinimumBalance,
// ...
// )
if (Provable.inCheckedComputation())
throw new Error('cannot call minimumBalanceAtSlot from a checked computation');
if (globalSlot.lessThan(this.cliffTime).toBoolean()) {
return this.initialMinimumBalance;
}
else if (this.vestingPeriod.equals(UInt32.zero).toBoolean()) {
return UInt64.zero;
}
else if (this.initialMinimumBalance.lessThan(this.cliffAmount).toBoolean()) {
return UInt64.zero;
}
else {
const minBalanceAfterCliff = this.initialMinimumBalance.sub(this.cliffAmount);
const numPeriodsVested = globalSlot.sub(this.cliffTime).div(this.vestingPeriod).toUInt64();
const vestingDecrementWillOverflow = !numPeriodsVested.equals(UInt64.zero).toBoolean() &&
UInt64.MAXINT().div(numPeriodsVested).lessThan(this.vestingIncrement).toBoolean();
const vestingDecrement = vestingDecrementWillOverflow
? UInt64.MAXINT()
: numPeriodsVested.mul(this.vestingIncrement);
if (minBalanceAfterCliff.lessThan(vestingDecrement).toBoolean()) {
return UInt64.zero;
}
else {
return minBalanceAfterCliff.sub(vestingDecrement);
}
}
}
static empty() {
return new AccountTiming({
initialMinimumBalance: UInt64.empty(),
cliffTime: UInt32.empty(),
cliffAmount: UInt64.empty(),
vestingPeriod: UInt32.empty(),
vestingIncrement: UInt64.empty(),
});
}
}
class Account {
constructor(State, isNew, data) {
this.State = State;
this.isNew = isNew instanceof Unconstrained ? isNew : Unconstrained.from(isNew);
this.accountId = data.accountId;
this.tokenSymbol = data.tokenSymbol;
this.balance = data.balance;
this.nonce = data.nonce;
this.receiptChainHash = data.receiptChainHash;
this.delegate = data.delegate;
this.votingFor = data.votingFor;
this.timing = data.timing;
this.permissions = data.permissions;
this.zkapp = {
state: data.zkapp?.state ?? StateValues.empty(this.State),
verificationKey: data.zkapp?.verificationKey ?? VerificationKey.dummySync(),
actionState: [new Field(0), new Field(0), new Field(0), new Field(0), new Field(0)], // TODO NOW
isProven: data.zkapp?.isProven ?? new Bool(false),
zkappUri: data.zkapp?.zkappUri ?? ZkappUri.empty(),
};
}
/*
checkAndApplyFeePayment(
feePayment: ZkappFeePayment
):
| { status: 'Applied'; updatedAccount: Account<State> }
| { status: 'Failed'; errors: Error[] } {
const errors: Error[] = [];
if (this.accountId.tokenId.equals(TokenId.MINA).not().toBoolean())
errors.push(new Error('cannot pay zkapp fee with a non-mina account'));
if (this.accountId.publicKey.equals(feePayment.publicKey).not().toBoolean())
errors.push(
new Error('fee payment public key does not match account public key')
);
if (this.nonce.equals(feePayment.nonce).not().toBoolean())
errors.push(new Error('invalid account nonce'));
if (this.balance.lessThan(feePayment.fee).toBoolean())
errors.push(
new Error(
'account does not have enough balance to pay the required fee'
)
);
// TODO: validWhile (probably checked elsewhere)
if (errors.length === 0) {
const updatedAccount = new Account(this.State, false, {
...this,
balance: this.balance.sub(feePayment.fee),
nonce: this.nonce.add(UInt32.one),
});
return { status: 'Applied', updatedAccount };
} else {
return { status: 'Failed', errors };
}
}
// TODO: replay checks (probably live on the AccountUpdate itself, but needs to be called near this)
checkAndApplyUpdate<Event, Action>(
update: AccountUpdate<State, Event, Action>
):
| { status: 'Applied'; updatedAccount: Account<State> }
| { status: 'Failed'; errors: Error[] } {
const errors: Error[] = [];
if (this.accountId.equals(update.accountId).not().toBoolean())
errors.push(
new Error(
'account id in account update does not match actual account id'
)
);
// TODO: check verificationKeyHash
// TODO: check mayUseToken (somewhere, maybe not here)
// CHECK PRECONDITIONS
function preconditionError(
preconditionName: string,
constraint: { toStringHuman(): string },
value: unknown
): Error {
return new Error(
`${preconditionName} precondition failed: ${value} does not satisfy "${constraint.toStringHuman()}"`
);
}
// WARNING: failing to specify the type parameter on this function exhibits unsound behavior
// (thanks typescript)
function checkPrecondition<T>(
preconditionName: string,
constraint: { isSatisfied(x: T): Bool; toStringHuman(): string },
value: T
): void {
if (constraint.isSatisfied(value).not().toBoolean())
errors.push(preconditionError(preconditionName, constraint, value));
}
checkPrecondition<UInt64>(
'balance',
update.preconditions.account.balance,
this.balance
);
checkPrecondition<UInt32>(
'nonce',
update.preconditions.account.nonce,
this.nonce
);
checkPrecondition<Field>(
'receiptChainHash',
update.preconditions.account.receiptChainHash,
this.receiptChainHash
);
if (this.delegate !== null)
checkPrecondition<PublicKey>(
'delegate',
update.preconditions.account.delegate,
this.delegate
);
checkPrecondition<Bool>(
'isProven',
update.preconditions.account.isProven,
this.zkapp.isProven
);
StateValues.checkPreconditions(
this.State,
this.zkapp.state,
update.preconditions.account.state
);
const actionState = this.zkapp?.actionState ?? [];
const actionStateSatisfied = Bool.anyTrue(
actionState.map((s) =>
update.preconditions.account.actionState.isSatisfied(s)
)
);
if (actionStateSatisfied.not().toBoolean())
errors.push(
preconditionError(
'actionState',
update.preconditions.account.actionState,
actionState
)
);
// TODO: updates.preconditions.account.isNew
// TODO: network (probably checked elsewhere)
// TODO: validWhile (probably checked elsewhere)
// CHECK PERMISSIONS
function checkPermission(
permissionName: string,
requiredAuthLevel: AuthorizationLevel,
actionIsPerformed: boolean
): void {
if(actionIsPerformed && !requiredAuthLevel.isSatisfied(update.authorizationKind))
errors.push(new Error(
`${permissionName} permission was violated: account update has authorization kind ${update.authorizationKind.identifier()}, but required auth level is ${requiredAuthLevel.identifier()}`
));
}
checkPermission('access', this.permissions.access, true);
checkPermission('send', this.permissions.send, update.balanceChange.isNegative().toBoolean());
checkPermission('receive', this.permissions.receive, update.balanceChange.isPositive().toBoolean());
checkPermission('incrementNonce', this.permissions.incrementNonce, update.incrementNonce.toBoolean());
checkPermission('setDelegate', this.permissions.setDelegate, update.delegateUpdate.set.toBoolean());
checkPermission('setPermissions', this.permissions.setPermissions, update.permissionsUpdate.set.toBoolean());
checkPermission('setVerificationKey', this.permissions.setVerificationKey.auth, update.verificationKeyUpdate.set.toBoolean());
checkPermission('setZkappUri', this.permissions.setZkappUri, update.zkappUriUpdate.set.toBoolean());
checkPermission('setTokenSymbol', this.permissions.setTokenSymbol, update.tokenSymbolUpdate.set.toBoolean());
checkPermission('setVotingFor', this.permissions.setVotingFor, update.votingForUpdate.set.toBoolean());
checkPermission('setTiming', this.permissions.setTiming, update.timingUpdate.set.toBoolean());
checkPermission('editActionState', this.permissions.editActionState, update.pushActions.data.length > 0);
checkPermission('editState', this.permissions.editState, StateUpdates.anyValuesAreSet(update.stateUpdates).toBoolean());
// APPLY UPDATES
// TODO: account for implicitAccountCreationFee here
let updatedBalance: UInt64 = this.balance;
// TODO: why is Int64 not comparable?
// if(update.balanceChange.lessThan(Int64.create(this.balance, Sign.minusOne)).toBoolean())
if (
update.balanceChange.isNegative().toBoolean() &&
update.balanceChange.magnitude.greaterThan(this.balance).toBoolean()
) {
errors.push(
new Error(
`insufficient balance for balanceChange (balance = ${this.balance}, balanceChange = -${update.balanceChange.magnitude})`
)
);
} else {
// TODO: check for overflows?
const isPos = update.balanceChange.isPositive().toBoolean();
const amount = update.balanceChange.magnitude;
updatedBalance = isPos
? this.balance.add(amount)
: this.balance.sub(amount);
}
// TODO: pushEvents
// TODO: pushActions
if (errors.length === 0) {
function applyUpdate<T>(update: Update<T>, value: T): T {
return update.set.toBoolean() ? update.value : value;
}
const allStateUpdated = Bool.allTrue(
StateUpdates.toFieldUpdates(this.State, update.stateUpdates).map(
(update) => update.set
)
);
const updatedAccount = new Account(this.State, false, {
...this,
balance: updatedBalance,
tokenSymbol: applyUpdate(update.tokenSymbolUpdate, this.tokenSymbol),
nonce: update.incrementNonce.toBoolean()
? this.nonce.add(UInt32.one)
: this.nonce,
delegate: applyUpdate(update.delegateUpdate, this.delegate),
votingFor: applyUpdate(update.votingForUpdate, this.votingFor),
timing: applyUpdate(update.timingUpdate, this.timing),
permissions: applyUpdate(update.permissionsUpdate, this.permissions),
zkapp: {
state: StateValues.applyUpdates(
this.State,
this.zkapp.state,
update.stateUpdates
),
verificationKey: applyUpdate(
update.verificationKeyUpdate,
this.zkapp.verificationKey
),
// actionState: TODO,
isProven: this.zkapp.isProven.or(allStateUpdated),
zkappUri: applyUpdate(update.zkappUriUpdate, this.zkapp.zkappUri),
},
});
return { status: 'Applied', updatedAccount };
} else {
return { status: 'Failed', errors };
}
}
*/
toGeneric() {
return new Account('GenericState', this.isNew, {
...this,
zkapp: {
...this.zkapp,
state: StateValues.toGeneric(this.State, this.zkapp.state),
},
});
}
static fromGeneric(account, State) {
return new Account(State, account.isNew, {
...account,
zkapp: {
...account.zkapp,
state: StateValues.fromGeneric(account.zkapp.state, State),
},
});
}
static empty(accountId) {
return new Account('GenericState', true, {
accountId,
tokenSymbol: TokenSymbol.empty(),
balance: UInt64.zero,
nonce: UInt32.zero,
receiptChainHash: new Field(0), // ReceiptChainHash.empty()
delegate: null,
votingFor: new Field(0),
timing: AccountTiming.empty(),
permissions: Permissions.defaults(),
});
}
}
//# sourceMappingURL=account.js.map