-
Notifications
You must be signed in to change notification settings - Fork 133
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
69472cb
commit 422f00d
Showing
1 changed file
with
86 additions
and
116 deletions.
There are no files selected for viewing
202 changes: 86 additions & 116 deletions
202
examples/zkapps/11-advanced-account-updates/src/WrappedMina.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,155 +1,125 @@ | ||
import { | ||
Bool, | ||
DeployArgs, | ||
Int64, | ||
method, | ||
Mina, | ||
AccountUpdate, | ||
Permissions, | ||
PublicKey, | ||
UInt64, | ||
State, | ||
state, | ||
TokenContract, | ||
AccountUpdateForest, | ||
AccountUpdateTree, | ||
assert, | ||
Bool, | ||
Reducer, | ||
TokenId, | ||
Provable, | ||
Types, | ||
Permissions, | ||
} from 'o1js'; | ||
|
||
export class WrappedMina extends TokenContract { | ||
async deploy(args?: DeployArgs) { | ||
await super.deploy(args); | ||
this.account.permissions.set({ | ||
...Permissions.default(), | ||
send: Permissions.proof(), | ||
}); | ||
} | ||
export { WrappedMina }; | ||
|
||
@state(UInt64) priorMina = State<UInt64>(); | ||
const $MINA = TokenId.default; | ||
|
||
// ---------------------------------------------------------------------- | ||
class WrappedMina extends TokenContract { | ||
@state(UInt64) totalSupply = State<UInt64>(); | ||
|
||
@method async init() { | ||
super.init(); | ||
// actions are total supply changes triggered by minting or burning | ||
totalSupplyReducer = Reducer({ actionType: Int64 }); | ||
|
||
let receiver = this.internal.mint({ | ||
address: this.address, | ||
amount: UInt64.from(0), | ||
}); | ||
// require that the receiving account is new, so this can be only done once | ||
receiver.account.isNew.requireEquals(Bool(true)); | ||
// pay fees for opened account | ||
this.balance.subInPlace(Mina.getNetworkConstants().accountCreationFee); | ||
this.priorMina.set(UInt64.from(0)); | ||
@method async init() { | ||
super.init(); // totalSupply provable starts at 0 | ||
} | ||
|
||
// ---------------------------------------------------------------------- | ||
async approveBase(forest: AccountUpdateForest) { | ||
this.checkZeroBalanceChange(forest); | ||
get $wMINA() { | ||
return this.deriveTokenId(); | ||
} | ||
|
||
// ---------------------------------------------------------------------- | ||
@method async mintWrappedMina(amount: UInt64, destination: PublicKey) { | ||
const priorMina = this.priorMina.get(); | ||
this.priorMina.requireEquals(this.priorMina.get()); | ||
|
||
const newMina = amount.add(priorMina); | ||
|
||
// TODO is there a way to directly get the balance change for this transaction? | ||
this.account.balance.requireBetween(newMina, UInt64.MAXINT()); | ||
// approve any transaction which leaves the token supply unchanged | ||
@method async approveBase(forest: AccountUpdateForest) { | ||
let sum = Int64.from(0); | ||
|
||
this.internal.mint({ address: destination, amount }); | ||
this.forEachUpdate(forest, (update, usesToken) => { | ||
sum = Provable.if(usesToken, sum.add(update.balanceChange), sum); | ||
checkPermissionsUpdate(update); | ||
}); | ||
|
||
this.priorMina.set(newMina); | ||
sum.assertEquals(Int64.zero); | ||
} | ||
|
||
// ---------------------------------------------------------------------- | ||
|
||
@method async redeemWrappedMinaApprove( | ||
burnWMINA: AccountUpdate, | ||
amount: UInt64 | ||
) { | ||
// check that the burn account update has our token id | ||
burnWMINA.body.tokenId.assertEquals(this.tokenId); | ||
@method async wrap(sender: AccountUpdateTree) { | ||
// get sender update and ensure there are no other updates | ||
assert(sender.children.isEmpty()); | ||
let senderUpdate = sender.accountUpdate.unhash(); | ||
this.approve(sender); | ||
|
||
// approve burn with at most 2 child account updates, which don't get token permissions | ||
this.approve(burnWMINA); | ||
// ensure sender is giving away a positive amount of MINA | ||
senderUpdate.tokenId.assertEquals($MINA); | ||
let amount = senderUpdate.balanceChange.neg(); | ||
assert(amount.isPositive()); | ||
|
||
// check that the account update burns the specified amount | ||
let balanceChange = Int64.fromObject(burnWMINA.body.balanceChange); | ||
balanceChange.assertEquals(Int64.from(amount).neg()); | ||
// move MINA from sender to this contract | ||
this.balance.addInPlace(amount); | ||
|
||
// in return for burn, decrease our MINA balance (can be picked up as balance increase anywhere it suits the caller) | ||
this.balance.subInPlace(amount); | ||
// mint same amount of wrapped MINA to sender | ||
let senderTokenUpdate = this.internal.mint({ | ||
address: senderUpdate.publicKey, | ||
amount: amount.magnitude, | ||
}); | ||
// allow minting to pay for account creation if the account doesn't exist | ||
senderTokenUpdate.body.implicitAccountCreationFee = Bool(true); | ||
|
||
// update priorMina | ||
const priorMina = this.priorMina.get(); | ||
this.priorMina.requireEquals(this.priorMina.get()); | ||
const newMina = priorMina.sub(amount); | ||
this.priorMina.set(newMina); | ||
// increase total wMINA supply | ||
this.totalSupplyReducer.dispatch(amount); | ||
} | ||
|
||
// ---------------------------------------------------------------------- | ||
|
||
@method async redeemWrappedMinaWithoutApprove( | ||
source: PublicKey, | ||
destination: PublicKey, | ||
amount: UInt64 | ||
) { | ||
this.internal.burn({ address: source, amount }); | ||
|
||
const priorMina = this.priorMina.get(); | ||
this.priorMina.requireEquals(this.priorMina.get()); | ||
|
||
const newMina = priorMina.sub(amount); | ||
|
||
this.send({ to: destination, amount }); | ||
|
||
this.priorMina.set(newMina); | ||
} | ||
@method async unwrap(sender: AccountUpdateTree) { | ||
/// get sender update and ensure there are no other updates | ||
assert(sender.children.isEmpty()); | ||
let senderTokenUpdate = sender.accountUpdate.unhash(); | ||
checkPermissionsUpdate(senderTokenUpdate); | ||
this.approve(sender); | ||
|
||
// ensure sender is burning a positive amount of wrapped MINA | ||
senderTokenUpdate.tokenId.assertEquals(this.$wMINA); | ||
let amount = senderTokenUpdate.balanceChange.neg(); | ||
assert(amount.isPositive()); | ||
|
||
// release same amount of MINA in return for burning | ||
let senderUpdate = this.send({ | ||
to: senderTokenUpdate.publicKey, | ||
amount: amount.magnitude, | ||
}); | ||
// allow sending to pay for account creation if the account doesn't exist | ||
senderUpdate.body.implicitAccountCreationFee = Bool(true); | ||
|
||
// ---------------------------------------------------------------------- | ||
|
||
// let a zkapp send tokens to someone, provided the token supply stays constant | ||
@method async approveUpdateAndSend( | ||
zkappUpdate: AccountUpdate, | ||
to: PublicKey, | ||
amount: UInt64 | ||
) { | ||
this.approve(zkappUpdate); // TODO is this secretly approving other changes? | ||
|
||
// see if balance change cancels the amount sent | ||
let balanceChange = Int64.fromObject(zkappUpdate.body.balanceChange); | ||
balanceChange.assertEquals(Int64.from(amount).neg()); | ||
// add same amount of tokens to the receiving address | ||
this.internal.mint({ address: to, amount }); | ||
// decrease total wMINA supply | ||
this.totalSupplyReducer.dispatch(amount.neg()); | ||
} | ||
|
||
// ---------------------------------------------------------------------- | ||
|
||
// let a zkapp do anything, provided the token supply stays constant | ||
@method async approveUpdate(zkappUpdate: AccountUpdate) { | ||
this.approve(zkappUpdate); // TODO is this secretly approving other changes? | ||
|
||
// see if balance change is zero | ||
let balanceChange = Int64.fromObject(zkappUpdate.body.balanceChange); | ||
balanceChange.assertEquals(Int64.from(0)); | ||
@method.returns(UInt64) async getBalance(publicKey: PublicKey) { | ||
let accountUpdate = AccountUpdate.create(publicKey, this.$wMINA); | ||
return accountUpdate.account.balance.getAndRequireEquals(); | ||
} | ||
} | ||
|
||
// ---------------------------------------------------------------------- | ||
function checkPermissionsUpdate(update: AccountUpdate) { | ||
let permissions = update.update.permissions; | ||
|
||
@method async transfer(from: PublicKey, to: PublicKey, value: UInt64) { | ||
this.internal.send({ from, to, amount: value }); | ||
} | ||
// account must not change its permissions in a way that prevents sending to it | ||
let { access, receive } = permissions.value; | ||
let accessIsNone = permissionEquals(access, Permissions.none()); | ||
let receiveIsNone = permissionEquals(receive, Permissions.none()); | ||
let updateAllowed = accessIsNone.and(receiveIsNone); | ||
|
||
// ---------------------------------------------------------------------- | ||
|
||
@method async getBalance(publicKey: PublicKey): UInt64 { | ||
let accountUpdate = AccountUpdate.create(publicKey, this.tokenId); | ||
let balance = accountUpdate.account.balance.get(); | ||
accountUpdate.account.balance.requireEquals( | ||
accountUpdate.account.balance.get() | ||
); | ||
return balance; | ||
} | ||
// either do an allowed update, or don't change permissions | ||
assert(updateAllowed.or(permissions.isSome.not())); | ||
} | ||
|
||
// ---------------------------------------------------------------------- | ||
function permissionEquals(p1: Types.AuthRequired, p2: Types.AuthRequired) { | ||
return p1.constant | ||
.equals(p2.constant) | ||
.and(p1.signatureNecessary.equals(p2.signatureNecessary)) | ||
.and(p1.signatureSufficient.equals(p2.signatureSufficient)); | ||
} |