@minatokens/nft
Version:
Mina NFT Standard draft library
295 lines (267 loc) • 9.06 kB
text/typescript
import {
AccountUpdate,
DeployArgs,
method,
Permissions,
PublicKey,
State,
state,
UInt64,
SmartContract,
Bool,
Field,
Struct,
Poseidon,
Provable,
} from "o1js";
import { Whitelist, OffChainList, Storage } from "@minatokens/storage";
import {
NFTAddress,
SellEvent,
DepositEvent,
WithdrawEvent,
BidEvent,
} from "./types.js";
import {
NFTCollectionBase,
NFTCollectionContractConstructor,
TransferParams,
UInt64Option,
NFTTransactionContext,
} from "../interfaces/index.js";
export class Bid extends Struct({
price: UInt64,
points: UInt64,
}) {
pack() {
return Field.fromBits([
...this.price.value.toBits(64),
...this.points.value.toBits(64),
]);
}
static unpack(field: Field): Bid {
const bits = field.toBits(64 + 64);
const price = UInt64.Unsafe.fromField(Field.fromBits(bits.slice(0, 64)));
const points = UInt64.Unsafe.fromField(
Field.fromBits(bits.slice(64, 64 + 64))
);
return new Bid({
price,
points,
});
}
}
export interface NonFungibleTokenBidContractDeployProps
extends Exclude<DeployArgs, undefined> {
/** The whitelist. */
whitelist: Field;
/** The offers. */
bids: Field;
/** The storage. */
storage: Storage;
}
export function BidFactory(params: {
collectionContract: () => NFTCollectionContractConstructor;
}) {
const { collectionContract } = params;
class NonFungibleTokenBidContract extends SmartContract {
buyer = State<PublicKey>();
whitelist = State<Field>();
bids = State<Field>();
storage = State<Storage>();
maxPoints = State<UInt64>();
// We do not have concurrency here, but given that it is a single-user contract,
// we can use state to track the points that have been consumed
// In case of concurrency, we can use the tokens to track the points
consumedPoints = State<UInt64>();
async deploy(args: NonFungibleTokenBidContractDeployProps) {
await super.deploy(args);
this.whitelist.set(args.whitelist);
this.bids.set(args.bids);
this.storage.set(args.storage);
this.account.permissions.set({
...Permissions.default(),
send: Permissions.proof(),
setVerificationKey:
Permissions.VerificationKey.impossibleDuringCurrentVersion(),
setPermissions: Permissions.impossible(),
});
}
events = {
deposit: DepositEvent,
withdraw: WithdrawEvent,
sell: SellEvent,
updateWhitelist: Whitelist,
bid: BidEvent,
};
async initialize(amount: UInt64, maxPoints: UInt64) {
this.account.provedState.requireEquals(Bool(false));
const buyer = this.sender.getUnconstrained();
const buyerUpdate = AccountUpdate.createSigned(buyer);
// We use low-level subInPlace and addInPlace to decrease the number of AccountUpdates
buyerUpdate.balance.subInPlace(amount.add(UInt64.from(1_000_000_000)));
this.self.balance.addInPlace(amount);
buyerUpdate.body.useFullCommitment = Bool(true);
this.buyer.set(buyer);
this.maxPoints.set(maxPoints);
this.emitEvent(
"deposit",
new DepositEvent({
buyer,
amount,
maxPoints,
})
);
}
getCollectionContract(address: PublicKey): NFTCollectionBase {
const CollectionContract = collectionContract();
return new CollectionContract(address);
}
async deposit(amount: UInt64, maxPoints: UInt64) {
amount.equals(UInt64.from(0)).assertFalse();
const sender = this.sender.getUnconstrained();
const buyer = this.buyer.getAndRequireEquals();
sender.assertEquals(buyer);
const buyerUpdate = AccountUpdate.createSigned(buyer);
buyerUpdate.send({ to: this.address, amount });
buyerUpdate.body.useFullCommitment = Bool(true);
this.maxPoints.set(maxPoints);
this.emitEvent(
"deposit",
new DepositEvent({
buyer,
amount,
maxPoints,
})
);
}
async withdraw(amount: UInt64, maxPoints: UInt64) {
amount.equals(UInt64.from(0)).assertFalse();
this.account.balance.requireBetween(amount, UInt64.MAXINT());
const buyer = this.buyer.getAndRequireEquals();
const sender = this.sender.getUnconstrained();
const senderUpdate = AccountUpdate.createSigned(sender);
senderUpdate.body.useFullCommitment = Bool(true);
sender.assertEquals(buyer);
let bidUpdate = this.send({ to: senderUpdate, amount });
bidUpdate.body.useFullCommitment = Bool(true);
this.maxPoints.set(maxPoints);
this.emitEvent(
"withdraw",
new WithdrawEvent({
buyer,
amount,
maxPoints,
})
);
}
async sell(nftAddress: NFTAddress, price: UInt64) {
await this._sell(nftAddress, price);
const buyer = this.buyer.getAndRequireEquals();
const Collection = collectionContract();
const collection = new Collection(nftAddress.collection);
await collection.transferBySignature(
new TransferParams({
address: nftAddress.nft,
from: PublicKey.empty(),
to: buyer,
price: UInt64Option.fromValue(price),
context: new NFTTransactionContext({
custom: [Field(0), Field(0), Field(0)],
}),
})
);
}
async approvedSell(nftAddress: NFTAddress, price: UInt64) {
await this._sell(nftAddress, price);
const buyer = this.buyer.getAndRequireEquals();
const Collection = collectionContract();
const collection = new Collection(nftAddress.collection);
await collection.approvedTransferBySignature(
new TransferParams({
address: nftAddress.nft,
from: PublicKey.empty(),
to: buyer,
price: UInt64Option.fromValue(price),
context: new NFTTransactionContext({
custom: [Field(0), Field(0), Field(0)],
}),
})
);
}
async _sell(nftAddress: NFTAddress, price: UInt64) {
price.equals(UInt64.from(0)).assertFalse();
const key = Poseidon.hashPacked(NFTAddress, nftAddress);
const storage = this.storage.getAndRequireEquals();
const bids = new OffChainList({
root: this.bids.getAndRequireEquals(),
storage,
});
const bid = Bid.unpack(
(await bids.getValue(key, "bids")).assertSome("bid not found")
);
// We do not require the price to be equal to the bid price,
// because the price can be lower than the bid price
// and the seller can still be willing to sell the NFT
// as the deposit remaining is less than bid price
price.assertLessThanOrEqual(bid.price, "price is too high");
this.account.balance.requireBetween(price, UInt64.MAXINT());
const consumedPoints = this.consumedPoints.getAndRequireEquals();
const maxPoints = this.maxPoints.getAndRequireEquals();
const newConsumedPoints = consumedPoints.add(bid.points);
newConsumedPoints.assertLessThanOrEqual(
maxPoints,
"consumed points exceed max points"
);
this.consumedPoints.set(newConsumedPoints);
const seller = this.sender.getUnconstrained();
const sellerUpdate = AccountUpdate.createSigned(seller);
sellerUpdate.balance.addInPlace(price);
this.self.balance.subInPlace(price);
sellerUpdate.body.useFullCommitment = Bool(true);
const whitelist = new Whitelist({
list: new OffChainList({
root: this.whitelist.getAndRequireEquals(),
storage,
}),
});
const whitelistedAmount = await whitelist.getWhitelistedAmount(
seller,
"whitelist"
);
const whitelistDisabled = whitelist.isNone();
whitelistedAmount.isSome
.or(whitelistDisabled)
.assertTrue("Cannot buy from non-whitelisted address");
const maxPrice = Provable.if(
whitelistDisabled,
UInt64.MAXINT(),
whitelistedAmount.value
);
price.assertLessThanOrEqual(
maxPrice,
"price is higher than whitelisted price"
);
this.emitEvent(
"sell",
new SellEvent({
collection: nftAddress.collection,
nft: nftAddress.nft,
price,
})
);
}
async bid(bids: Field, whitelist: Field, storage: Storage) {
const buyer = this.buyer.getAndRequireEquals();
const sender = this.sender.getUnconstrained();
const senderUpdate = AccountUpdate.createSigned(sender);
senderUpdate.body.useFullCommitment = Bool(true);
sender.assertEquals(buyer);
this.bids.set(bids);
this.whitelist.set(whitelist);
this.storage.set(storage);
this.emitEvent("bid", new BidEvent({ bids, whitelist, storage }));
}
}
return NonFungibleTokenBidContract;
}