@silvana-one/nft
Version:
Mina NFT library
439 lines • 20.7 kB
JavaScript
import { __decorate, __metadata } from "tslib";
import { AccountUpdate, method, Permissions, PublicKey, State, state, UInt64, SmartContract, Bool, UInt32, Field, Struct, assert, Provable, } from "o1js";
import { UInt64Option, TransferEvent, NFTTransactionContext, TransferExtendedParams, TransferByProofParams, } from "../interfaces/index.js";
import { mulDiv } from "../util/index.js";
const MAX_SALE_FEE = 100000;
const MIN_STEP = 10; // 1% to previous bid
export class AuctionPacked extends Struct({
ownerX: Field,
collectionX: Field,
nftX: Field,
auctioneerX: Field,
bidderX: Field,
data: Field,
}) {
}
export class Auction extends Struct({
owner: PublicKey,
collection: PublicKey,
nft: PublicKey,
auctioneer: PublicKey,
bidder: PublicKey,
minimumPrice: UInt64,
transferFee: UInt64,
/** The sale fee percentage (e.g., 1000 = 1%, 100 = 0.1%, 10000 = 10%, 100000 = 100%). */
saleFee: UInt32,
auctionEndTime: UInt32,
withdrawPeriod: UInt32, // in slots
isOwnerPaid: Bool,
isNFTtransferred: Bool,
isNFTwithdrawn: Bool,
}) {
pack() {
const data = Field.fromBits([
...this.minimumPrice.value.toBits(64),
...this.transferFee.value.toBits(64),
...this.saleFee.value.toBits(32),
...this.auctionEndTime.value.toBits(32),
...this.withdrawPeriod.value.toBits(32),
this.owner.isOdd,
this.collection.isOdd,
this.nft.isOdd,
this.auctioneer.isOdd,
this.bidder.isOdd,
this.isOwnerPaid,
this.isNFTtransferred,
this.isNFTwithdrawn,
]);
return new AuctionPacked({
ownerX: this.owner.x,
collectionX: this.collection.x,
nftX: this.nft.x,
auctioneerX: this.auctioneer.x,
bidderX: this.bidder.x,
data,
});
}
static unpack(packed) {
const bits = packed.data.toBits(64 + 64 + 32 + 32 + 32 + 8);
const ownerX = packed.ownerX;
const collectionX = packed.collectionX;
const nftX = packed.nftX;
const auctioneerX = packed.auctioneerX;
const bidderX = packed.bidderX;
const ownerIsOdd = bits[64 + 64 + 32 + 32 + 32];
const collectionIsOdd = bits[64 + 64 + 32 + 32 + 32 + 1];
const nftIsOdd = bits[64 + 64 + 32 + 32 + 32 + 2];
const auctioneerIsOdd = bits[64 + 64 + 32 + 32 + 32 + 3];
const bidderIsOdd = bits[64 + 64 + 32 + 32 + 32 + 4];
const isOwnerPaid = bits[64 + 64 + 32 + 32 + 32 + 5];
const isNFTtransferred = bits[64 + 64 + 32 + 32 + 32 + 6];
const isNFTwithdrawn = bits[64 + 64 + 32 + 32 + 32 + 7];
const owner = PublicKey.from({ x: ownerX, isOdd: ownerIsOdd });
const collection = PublicKey.from({
x: collectionX,
isOdd: collectionIsOdd,
});
const nft = PublicKey.from({ x: nftX, isOdd: nftIsOdd });
const auctioneer = PublicKey.from({
x: auctioneerX,
isOdd: auctioneerIsOdd,
});
const bidder = PublicKey.from({
x: bidderX,
isOdd: bidderIsOdd,
});
const minimumPrice = UInt64.Unsafe.fromField(Field.fromBits(bits.slice(0, 64)));
const transferFee = UInt64.Unsafe.fromField(Field.fromBits(bits.slice(64, 64 + 64)));
const saleFee = UInt32.Unsafe.fromField(Field.fromBits(bits.slice(64 + 64, 64 + 64 + 32)));
const auctionEndTime = UInt32.Unsafe.fromField(Field.fromBits(bits.slice(64 + 64 + 32, 64 + 64 + 32 + 32)));
const withdrawPeriod = UInt32.Unsafe.fromField(Field.fromBits(bits.slice(64 + 64 + 32 + 32, 64 + 64 + 32 + 32 + 32)));
return new Auction({
owner,
collection,
nft,
auctioneer,
bidder,
minimumPrice,
transferFee,
saleFee,
auctionEndTime,
withdrawPeriod,
isOwnerPaid,
isNFTtransferred,
isNFTwithdrawn,
});
}
}
export class AuctionState extends Struct({
bidAmount: UInt64,
auction: Auction,
settled: Bool,
}) {
}
export class AuctionBidEvent extends Struct({
bidder: PublicKey,
price: UInt64,
}) {
}
/**
* Creates a new NFT Collection Contract class.
*
* @param params - Constructor parameters including admin and upgrade contracts, and network ID.
* @returns The Collection class extending TokenContract and implementing required interfaces.
*/
export function AuctionFactory(params) {
const { collectionContract } = params;
class NonFungibleTokenAuctionContract extends SmartContract {
constructor() {
super(...arguments);
this.auctionData = State();
this.bidAmount = State(UInt64.zero);
this.settled = State(Bool(false));
this.events = {
bid: AuctionBidEvent,
settleAuction: TransferByProofParams,
canTransfer: TransferEvent,
settlePayment: UInt64,
settleAuctioneerPayment: UInt64,
withdraw: UInt64,
};
}
async deploy(args) {
await super.deploy(args);
assert(args.saleFee.lessThanOrEqual(UInt32.from(MAX_SALE_FEE)), "Sale fee is too high");
this.auctionData.set(new Auction({
owner: args.owner,
collection: args.collection,
nft: args.nft,
auctioneer: args.auctioneer,
minimumPrice: args.minimumPrice,
transferFee: args.transferFee,
saleFee: args.saleFee,
auctionEndTime: args.auctionEndTime,
withdrawPeriod: args.withdrawPeriod,
bidder: PublicKey.empty(),
isOwnerPaid: Bool(false),
isNFTtransferred: Bool(false),
isNFTwithdrawn: Bool(false),
}).pack());
this.settled.set(Bool(false));
this.bidAmount.set(UInt64.zero);
this.account.permissions.set({
...Permissions.default(),
send: Permissions.proof(),
setVerificationKey: Permissions.VerificationKey.impossibleDuringCurrentVersion(),
setPermissions: Permissions.impossible(),
});
}
getCollectionContract(address) {
const CollectionContract = collectionContract();
return new CollectionContract(address);
}
calculateSaleFee(params) {
const { price, saleFee, transferFee } = params;
saleFee.assertLessThanOrEqual(UInt32.from(MAX_SALE_FEE), "Sale fee is too high");
return mulDiv({
value: price,
multiplier: UInt64.from(saleFee),
denominator: UInt64.from(MAX_SALE_FEE),
}).result;
}
// anyone can call this method to bid, paying the bid amount for the bidder
async bid(price, bidder) {
const settled = this.settled.getAndRequireEquals();
settled.assertFalse("Auction already finished");
const bidAmount = this.bidAmount.getAndRequireEquals();
this.account.balance.requireBetween(bidAmount, UInt64.MAXINT());
const auction = Auction.unpack(this.auctionData.getAndRequireEquals());
price.assertGreaterThanOrEqual(auction.minimumPrice, "Bid should be greater or equal than the minimum price");
price.assertGreaterThan(bidAmount.add(mulDiv({
value: bidAmount,
multiplier: UInt64.from(MIN_STEP),
denominator: UInt64.from(1000),
}).result), "Bid should be greater than the existing bid plus the minimum step");
this.network.globalSlotSinceGenesis.requireBetween(UInt32.from(0), auction.auctionEndTime);
const sender = this.sender.getUnconstrained();
const senderUpdate = AccountUpdate.createSigned(sender);
// if there is no bidder, this AccountUpdate will be ignored, similar to AccountUpdate.createIf()
const returnUpdate = AccountUpdate.create(auction.bidder);
senderUpdate.body.useFullCommitment = Bool(true);
returnUpdate.body.useFullCommitment = Bool(true);
// return the previous bidder's bid
this.balance.subInPlace(bidAmount);
returnUpdate.balance.addInPlace(bidAmount);
// get the new bid deposit
senderUpdate.balance.subInPlace(price);
this.balance.addInPlace(price);
this.bidAmount.set(price);
auction.bidder = bidder;
this.auctionData.set(auction.pack());
this.emitEvent("bid", new AuctionBidEvent({ bidder, price }));
return auction;
}
async getAuctionState() {
return new AuctionState({
auction: Auction.unpack(this.auctionData.getAndRequireEquals()),
bidAmount: this.bidAmount.getAndRequireEquals(),
settled: this.settled.getAndRequireEquals(),
});
}
// anyone can call this method to settle the auction
// but it is intended to be called by the auctioneer
// because the auctioneer is the one who will get the auction commission
// and pay the royalty to NFT creator
// This method is atomic, so it will settle the auction
async settleAuction() {
const settled = this.settled.getAndRequireEquals();
settled.assertFalse("Auction already settled");
this.settled.set(Bool(true));
const auction = Auction.unpack(this.auctionData.getAndRequireEquals());
this.network.globalSlotSinceGenesis.requireBetween(auction.auctionEndTime.add(1), UInt32.MAXINT());
const nftAddress = auction.nft;
const bidAmount = this.bidAmount.getAndRequireEquals();
auction.bidder.equals(PublicKey.empty()).assertFalse("No bidder");
bidAmount.assertGreaterThanOrEqual(auction.minimumPrice, "Bidder does not have enough balance");
const collection = this.getCollectionContract(auction.collection);
const transferParams = new TransferByProofParams({
address: nftAddress,
from: this.address,
to: auction.bidder,
price: UInt64Option.fromValue(bidAmount),
context: new NFTTransactionContext({
custom: [Field(1), Field(0), Field(0)],
}),
});
await collection.transferByProof(transferParams);
this.emitEvent("settleAuction", transferParams);
}
// and pay the royalty to NFT creator
// This method is atomic, so it will settle the auction
async withdrawNFT() {
const settled = this.settled.getAndRequireEquals();
settled.assertFalse("Auction already settled");
this.settled.set(Bool(true));
const auction = Auction.unpack(this.auctionData.getAndRequireEquals());
auction.isNFTwithdrawn.assertFalse("NFT already withdrawn");
auction.isNFTtransferred.assertFalse("NFT already transferred");
this.network.globalSlotSinceGenesis.requireBetween(auction.auctionEndTime.add(auction.withdrawPeriod), UInt32.MAXINT());
const nftAddress = auction.nft;
const collection = this.getCollectionContract(auction.collection);
const transferParams = new TransferByProofParams({
address: nftAddress,
from: this.address,
to: auction.owner,
price: UInt64Option.none(),
context: new NFTTransactionContext({
custom: [Field(2), Field(0), Field(0)],
}),
});
await collection.transferByProof(transferParams);
this.emitEvent("settleAuction", transferParams);
}
async canTransfer(params) {
this.settled.requireEquals(Bool(true));
const isSale = params.context.custom[0].equals(Field(1));
const isWithdraw = params.context.custom[0].equals(Field(2));
isSale.or(isWithdraw).assertTrue("Invalid context");
const auction = Auction.unpack(this.auctionData.getAndRequireEquals());
auction.isNFTtransferred.assertFalse("NFT already transferred");
auction.isNFTwithdrawn.assertFalse("NFT already withdrawn");
const collectionAddress = auction.collection;
const nftAddress = auction.nft;
const owner = auction.owner;
const bidder = auction.bidder;
const bidAmount = this.bidAmount.getAndRequireEquals();
this.network.globalSlotSinceGenesis.requireBetween(auction.auctionEndTime.add(Provable.if(isSale, UInt32.from(1), auction.withdrawPeriod)), UInt32.MAXINT());
params.collection.assertEquals(collectionAddress);
params.nft.assertEquals(nftAddress);
params.from
.equals(owner)
.and(params.approved.equals(this.address))
.or(params.from
.equals(this.address)
.and(params.approved.equals(PublicKey.empty())))
.assertTrue("Only owner or auction can transfer");
params.price.isSome.assertEquals(isSale);
params.price
.orElse(UInt64.zero)
.assertEquals(Provable.if(isSale, bidAmount, UInt64.zero));
params.price
.orElse(UInt64.MAXINT())
.assertGreaterThanOrEqual(auction.minimumPrice, "Bid should be greater or equal than the minimum price");
params.to.assertEquals(Provable.if(isSale, bidder, auction.owner));
const fee = params.fee.orElse(UInt64.zero);
fee.assertLessThanOrEqual(Provable.if(isSale, bidAmount, UInt64.MAXINT()), "Fee is higher than the bid");
const saleFee = this.calculateSaleFee({
price: bidAmount,
saleFee: auction.saleFee,
transferFee: auction.transferFee,
});
fee.assertLessThanOrEqual(Provable.if(isSale, saleFee, UInt64.MAXINT()), "Fee is higher than the sale fee");
auction.isNFTtransferred = isSale;
auction.isNFTwithdrawn = isWithdraw;
this.auctionData.set(auction.pack());
this.emitEvent("canTransfer", new TransferEvent({
...params,
}));
return Bool(true);
}
async settlePayment() {
this.settled.getAndRequireEquals().assertTrue("Auction not settled");
const auction = Auction.unpack(this.auctionData.getAndRequireEquals());
auction.isOwnerPaid.assertFalse("Owner is not paid yet");
auction.isNFTtransferred.assertTrue("NFT not transferred");
const bidAmount = this.bidAmount.getAndRequireEquals();
this.network.globalSlotSinceGenesis.requireBetween(auction.auctionEndTime.add(1), UInt32.MAXINT());
const payment = bidAmount.sub(this.calculateSaleFee({
price: bidAmount,
saleFee: auction.saleFee,
transferFee: auction.transferFee,
}));
this.account.balance.requireBetween(payment, UInt64.MAXINT());
const ownerUpdate = AccountUpdate.create(auction.owner);
ownerUpdate.balance.addInPlace(payment);
this.balance.subInPlace(payment);
ownerUpdate.body.useFullCommitment = Bool(true);
auction.isOwnerPaid = Bool(true);
this.auctionData.set(auction.pack());
this.emitEvent("settlePayment", payment);
}
/*
const balance = this.account.balance.getAndRequireEquals();
is not stable and sometimes gives 0 on devnet during proving, so we put the amount as a parameter
This method can be called many times by anyone, allowing the auctioneer to use the hardware wallet and bots
*/
async settleAuctioneerPayment(amount) {
this.settled.getAndRequireEquals().assertTrue("Auction not settled");
const auction = Auction.unpack(this.auctionData.getAndRequireEquals());
auction.isOwnerPaid.assertTrue("Owner is not paid yet, first call settlePayment");
this.network.globalSlotSinceGenesis.requireBetween(auction.auctionEndTime.add(1), UInt32.MAXINT());
this.account.balance.requireBetween(amount, UInt64.MAXINT());
const auctioneerUpdate = AccountUpdate.create(auction.auctioneer);
auctioneerUpdate.balance.addInPlace(amount);
this.balance.subInPlace(amount);
auctioneerUpdate.body.useFullCommitment = Bool(true);
this.emitEvent("settleAuctioneerPayment", amount);
}
/**
* Withdraw the deposit from the auction
* in case the auction is not settled during the WITHDRAW_PERIOD
* for any reason
* Anybody can call this method to allow the use of bots by the auctioneer or bidder
*/
async withdraw() {
const auction = Auction.unpack(this.auctionData.getAndRequireEquals());
auction.isNFTtransferred.assertFalse("NFT already transferred");
this.network.globalSlotSinceGenesis.requireBetween(auction.auctionEndTime.add(auction.withdrawPeriod), UInt32.MAXINT());
const bidAmount = this.bidAmount.getAndRequireEquals();
const bidderUpdate = AccountUpdate.create(auction.bidder);
bidderUpdate.balance.addInPlace(bidAmount);
this.balance.subInPlace(bidAmount);
bidderUpdate.body.useFullCommitment = Bool(true);
this.settled.set(Bool(true));
this.emitEvent("withdraw", bidAmount);
}
}
__decorate([
state(AuctionPacked),
__metadata("design:type", Object)
], NonFungibleTokenAuctionContract.prototype, "auctionData", void 0);
__decorate([
state(UInt64),
__metadata("design:type", Object)
], NonFungibleTokenAuctionContract.prototype, "bidAmount", void 0);
__decorate([
state(Bool),
__metadata("design:type", Object)
], NonFungibleTokenAuctionContract.prototype, "settled", void 0);
__decorate([
method.returns(Auction),
__metadata("design:type", Function),
__metadata("design:paramtypes", [UInt64, PublicKey]),
__metadata("design:returntype", Promise)
], NonFungibleTokenAuctionContract.prototype, "bid", null);
__decorate([
method.returns(AuctionState),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", Promise)
], NonFungibleTokenAuctionContract.prototype, "getAuctionState", null);
__decorate([
method,
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", Promise)
], NonFungibleTokenAuctionContract.prototype, "settleAuction", null);
__decorate([
method,
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", Promise)
], NonFungibleTokenAuctionContract.prototype, "withdrawNFT", null);
__decorate([
method.returns(Bool),
__metadata("design:type", Function),
__metadata("design:paramtypes", [TransferExtendedParams]),
__metadata("design:returntype", Promise)
], NonFungibleTokenAuctionContract.prototype, "canTransfer", null);
__decorate([
method,
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", Promise)
], NonFungibleTokenAuctionContract.prototype, "settlePayment", null);
__decorate([
method,
__metadata("design:type", Function),
__metadata("design:paramtypes", [UInt64]),
__metadata("design:returntype", Promise)
], NonFungibleTokenAuctionContract.prototype, "settleAuctioneerPayment", null);
__decorate([
method,
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", Promise)
], NonFungibleTokenAuctionContract.prototype, "withdraw", null);
return NonFungibleTokenAuctionContract;
}
//# sourceMappingURL=auction.js.map