o1js
Version:
TypeScript framework for zk-SNARKs and zkApps
123 lines (95 loc) • 3.43 kB
text/typescript
import {
Nullifier,
Field,
SmartContract,
state,
State,
method,
MerkleMap,
MerkleMapWitness,
Mina,
AccountUpdate,
Provable,
} from 'o1js';
class PayoutOnlyOnce extends SmartContract {
(Field) nullifierRoot = State<Field>();
(Field) nullifierMessage = State<Field>();
async payout(nullifier: Nullifier) {
let nullifierRoot = this.nullifierRoot.getAndRequireEquals();
let nullifierMessage = this.nullifierMessage.getAndRequireEquals();
// verify the nullifier
nullifier.verify([nullifierMessage]);
let nullifierWitness = Provable.witness(MerkleMapWitness, () =>
NullifierTree.getWitness(nullifier.key())
);
// we compute the current root and make sure the entry is set to 0 (= unused)
nullifier.assertUnusedV2(nullifierWitness, nullifierRoot);
// we set the nullifier to 1 (= used) and calculate the new root
let newRoot = nullifier.setUsedV2(nullifierWitness);
// we update the on-chain root
this.nullifierRoot.set(newRoot);
// we pay out a reward
let balance = this.account.balance.getAndRequireEquals();
let halfBalance = balance.div(2);
// finally, we send the payout to the public key associated with the nullifier
this.send({ to: nullifier.getPublicKey(), amount: halfBalance });
}
}
const NullifierTree = new MerkleMap();
let Local = await Mina.LocalBlockchain({ proofsEnabled: true });
Mina.setActiveInstance(Local);
// a test account that pays all the fees, and puts additional funds into the contract
let [sender] = Local.testAccounts;
// the contract account
let contractAccount = Mina.TestPublicKey.random();
// a special account that is allowed to pull out half of the contract balance, once
let privileged = Mina.TestPublicKey.random();
let initialBalance = 10_000_000_000;
let contract = new PayoutOnlyOnce(contractAccount);
// a unique message
let nullifierMessage = Field(5);
console.log('compile');
await PayoutOnlyOnce.compile();
console.log('deploy');
let tx = await Mina.transaction(sender, async () => {
let senderUpdate = AccountUpdate.fundNewAccount(sender);
senderUpdate.send({ to: contractAccount, amount: initialBalance });
await contract.deploy();
contract.nullifierRoot.set(NullifierTree.getRoot());
contract.nullifierMessage.set(nullifierMessage);
});
await tx.prove();
await tx.sign([sender.key, contractAccount.key]).send();
console.log(
`contract balance: ${contract.account.balance.get().div(1e9)} MINA`
);
console.log('generating nullifier');
let jsonNullifier = Nullifier.createTestNullifier(
[nullifierMessage],
privileged.key
);
console.log(jsonNullifier);
console.log('pay out');
tx = await Mina.transaction(sender, async () => {
AccountUpdate.fundNewAccount(sender);
await contract.payout(Nullifier.fromJSON(jsonNullifier));
});
await tx.prove();
await tx.sign([sender.key]).send();
console.log(`zkapp balance: ${contract.account.balance.get().div(1e9)} MINA`);
console.log(
`user balance: ${Mina.getAccount(privileged).balance.div(1e9)} MINA`
);
console.log('trying second pay out');
try {
tx = await Mina.transaction(sender, async () => {
await contract.payout(Nullifier.fromJSON(jsonNullifier));
});
await tx.prove();
await tx.sign([sender.key]).send();
} catch (error: any) {
console.log(
'transaction failed, as expected! received the following error message:'
);
console.log(error.message);
}