Skip to main content

Submitting a bid

This page covers two writes that happen before an auction can settle:

  1. Bidder: placeBid — seal a bid amount into the auction's CDR vault.
  2. Seller: sealReserve — seal the reserve price into the auction's CDR reserve vault.

Both helpers are Node-only (WASM constraint; see Installation).


Placing a bid: placeBid

placeBid executes the full four-on-chain-transaction flow in one call:

  1. WIP.approve(SealedAuction, deposit) — authorise the contract to pull the deposit.
  2. SealedAuction.allocateBidSlot(auctionId, deposit) — transfers WIP into escrow, allocates a CDR vault, emits BidSlotAllocated(uuid).
  3. Sign + encode the 149-byte BidPayload locally (no network).
  4. Encrypt with encryptBid({uuid, plaintext}) (TDH2).
  5. SealedAuction.submitEncryptedBid(auctionId, uuid, ciphertext) — writes ciphertext to the pre-allocated vault.
bid.ts
import {placeBid} from '@sealedip/sdk/bidder'
import {parseEther} from 'viem'

const result = await placeBid({
auctionId: 3n,
amount: parseEther('2.5'),
// deposit defaults to amount when omitted
bidderPrivateKey: process.env.BIDDER_KEY as `0x${string}`,
rpcUrl: 'https://aeneid.storyrpc.io',
})

console.log({
bidderAddress: result.bidderAddress,
uuid: result.uuid,
approveTxHash: result.approveTxHash,
allocateTxHash: result.allocateTxHash,
submitTxHash: result.submitTxHash,
})

PlaceBidInput

interface PlaceBidInput {
auctionId: bigint
amount: bigint // bid amount in wei (WIP)
/** Escrow deposit. Defaults to `amount` when omitted. */
deposit?: bigint
bidderPrivateKey: `0x${string}`
rpcUrl?: string // defaults to the Aeneid RPC
}

deposit can exceed amount — it is the ceiling visible to the contract. The bid can never win more than deposit, so setting deposit > amount is safe and hides the exact bid amount from observers who watch deposit events.

PlaceBidResult

interface PlaceBidResult {
bidderAddress: `0x${string}`
uuid: number // CDR vault uuid for this bid slot
nonce: `0x${string}` // 32-byte random nonce used in the payload
ciphertextHex: `0x${string}`
approveTxHash: `0x${string}`
allocateTxHash: `0x${string}`
submitTxHash: `0x${string}`
}

Multiple bids on the same auction

Call placeBid multiple times. Each call allocates a new slot with an independent deposit. At settle, the contract picks the highest valid bid across all slots.

Retry semantics

placeBid is not idempotent. If step 2 (allocate) succeeds and a later step fails, a re-run allocates a second slot and transfers a second deposit. To retry only the failed step, call submitEncryptedBid directly with the existing uuid from the first attempt.


Sealing the reserve: sealReserve

The seller seals the reserve price into the CDR reserve vault that createAuction allocated. This is optional: if the seller never calls sealReserve, the contract defaults the floor to 0 at settle.

sealReserve mirrors placeBid without the deposit/approve steps, because the reserve vault is allocated at auction creation.

seal-reserve.ts
import {sealReserve} from '@sealedip/sdk'
// also importable from '@sealedip/sdk/seller'
import {parseEther} from 'viem'

const result = await sealReserve({
auctionId: 3n,
reserve: parseEther('1.0'),
sellerPrivateKey: process.env.SELLER_KEY as `0x${string}`,
rpcUrl: 'https://aeneid.storyrpc.io',
})

console.log({
sellerAddress: result.sellerAddress,
reserveUuid: result.reserveUuid,
submitTxHash: result.submitTxHash,
})

What sealReserve does

  1. Calls getAuction(auctionId) to read reserveUuid.
  2. Signs the reserve amount with the same signBidDigest / personal_sign scheme as bids (signer field is the seller address).
  3. Encodes the 149-byte payload and TDH2-encrypts it bound to reserveUuid.
  4. Calls SealedAuction.submitEncryptedReserve(auctionId, ciphertext).

SealReserveInput

interface SealReserveInput {
auctionId: bigint
reserve: bigint // reserve price in wei (WIP)
sellerPrivateKey: `0x${string}`
rpcUrl?: string
}

SealReserveResult

interface SealReserveResult {
sellerAddress: `0x${string}`
reserveUuid: number // CDR vault uuid (same as Auction.reserveUuid)
nonce: `0x${string}`
ciphertextHex: `0x${string}`
submitTxHash: `0x${string}`
}

Reserve constraints

  • Seller-only: submitEncryptedReserve reverts with NotSeller for any other caller.
  • One-shot: once written, the reserve ciphertext cannot be overwritten (ReserveAlreadyWritten).
  • Must be called before deadline.
  • Auction must be in Open state.

Error reference

ErrorWhen it occurs
DepositTransferFailedWIP balance or approval was insufficient
BiddingClosedDeadline has passed; no new bids accepted
CiphertextAlreadyWrittenA bid for this slot was already submitted
ZeroDepositDeposit amount was 0
NotSellersubmitEncryptedReserve called by a non-seller address
ReserveAlreadyWrittenSeller attempted to overwrite the reserve ciphertext
UnknownAuctionauctionId does not exist

Reading auction state before bidding

import {getAuction} from '@sealedip/sdk/auction'

const auction = await getAuction(3n)
// auction.state must be 'Open'
// auction.deadline > Date.now() / 1000
// auction.reserveHasCiphertext is true once the seller has sealed the reserve

See Types: Auction for the full field list.