A universal NFT offer contract written in Tolk for the TON blockchain. Supports offers in both TON and Jettons (e.g. USDT).
contracts/— Tolk smart contract source codewrappers/— TypeScript wrapper classes for contract interactiontests/— Jest tests using@ton/sandboxscripts/— deployment scripts
npx blueprint build
npx blueprint test{
"hash": "f74dafa31cd3b8615c3cf67cdc464ac5b1708f87d6aad2dd32a3c6259a26bf5e",
"hashBase64": "902voxzTuGFcPPZ83EZKxbFwj4fWqtLdMqPGJZomv14=",
"hex": "b5ee9c72410217010005f9000114ff00f4a413f4bcf2c80b01020120021602014803150202ce0413020120051204913e247c903b5134348034c7f4c7f4c7fe923e923e923e80353d0142b5cb08cc98120b2497c2f835cb09f6bb7e8f38c0b5cb080a271b2338c0b5cb08e6c5a13938c0b5cb08000000072006070d0f00c2f8925006c705f89224c705b1f2e3e8296ef2d44d09d0fa48fa50d70bff016ef2e44e058308d718d74c20f900541027f910f2e44cd0fa48fa483002c8fa52fa5415cbffc908c8ca0017cb1f15cb1f13cb1ffa52fa52fa5201fa0212ccf400c9ed5402fe36296eb382100ee6b280821005f5e100e30406d33ffa4830f89226c7052bb33c500bb0f8975008be17b027f823bcb08ec453588e315bf8926d6dc8cf917f30f45215cb3f5230fa5213fa5212f400cf842012f400c9c8cf858812fa5271cf0b6eccc98040fb00ede3ba727fed118aed41edf101f2ffe010575f0732f8926d6d080c02fe20d0fa48d310fa50d310d15250820186a0a9845253820186a0a9845352a121a120c200f2e3ec2e6eb38e342ed0fa4831fa50d3ff31d1547be0045611f00121c20098545a2254166ff001923430e221c200216eb3b09528512cf001925f03e2e30e6d716dc8cf917f30f45219cb3f5250fa521bfa52f4005009fa0215f400c9090b01c48b650726f6669748c8cf9000000002cec9c8cf850852f0fa5258fa0271cf0b6accc971fb0020c2008e298ba4d61726b6574206665658c8cf9000000002cec9c8cf850815fa5201fa0271cf0b6a13ccc971fb00923032e220c200226eb3b0915be30d0a004a8b7526f79616c74798c8cf9000000002cec9c8cf850813fa5201fa0271cf0b6accc971fb00008ac8cf85885230fa5271cf0b6eccc98306fb00f823c8cf8327cf0b1f375257cb1f355255cb1f355215fa52315230fa52335213fa523121fa023121cf14315210f40031c9ed540058c8cf917f30f45215cb3f5230fa5213fa5212f400cf842012f400c9c8cf858812fa5271cf0b6eccc98040fb0001fed33ffa00fa48302c6e2cb18e356c9333f8926d6dc8cf903e29fa9615cb3f5003fa025240fa5214fa52f400cf8420f400c9c8cf858812fa5271cf0b6eccc98042fb00e02cd0fa4831fa50d3ff31d1206ef89258c705b3b15316c705b3b1e302303112a008c8ca0017cb1f15cb1f13cb1ffa52fa52fa5258fa02ccf400c9ed540e006a6c9333f8926d6dc8cf903e29fa9615cb3f5003fa025240fa5214fa52f400cf8420f400c9c8cf858812fa5271cf0b6eccc98042fb0002f88e5e3028f2d3e9f89223c705f89226c705b1f2e3e8f89225c7058e3df8978d06d3d999995c8818d85b98d95b08189e481b585c9ad95d1c1b1858d960c8cf9000000002cec9c8cf85085270fa5258fa0271cf0b6accc971fb00de5508f0025f0ae0d72c2000000004e302375f043334d72c200000115ce302840ff2f0101100a08b663616e63656c8c7058e1228f2d3e9f89223c705f2e3e85508f0025f0ae028f2d3e9f89223c705f2e3e8296ef2e44df89712a008c8ca0017cb1f15cb1f13cb1ffa52fa52fa5258fa02ccf400c9ed540070f89258c70512b0f2e3e8d74cd0d30722c3008e14810258f8235341a1bc04f82302a0b913b0f2d3eb9132e2208020b0f2d3ea01d74c01fb00007b0870406497c17820829896801b5b7233e40f8a7ea585f2cfd4013e80853e94be94bd0033e10804bd00327233e16204fe94807e809c73c2dab3325c7ec02001f5439286eb38e4728d0fa4831fa5030206e91308e386d6dc88bc0f8a7ea500000000000000008cf1625fa025260fa525260fa5212f400cf8420f400c9c8cf858812fa5271cf0b6eccc98306fb00e28e146dc8cf85085240fa5270cf0b6ef400c98306fb00e27fc8cf8329cf0b1f28cf0b1f27cf0b1f5260fa5252508140028fa525240fa5223fa0222cf1452a0f400c9ed540900aba04eefda89a1a401a63fa63fa63ff491f491f491f401a9e80a03a1f491a621f4a1ae1620a6a104030d415308a6c704030d415308a6e342d824434262418401e5c7d8dada4edd226f34b60ba1f491f4a060a0cdc4aa0b0058f230ed44d0d200d31fd31fd31ffa48fa48fa48fa00d4f40529f2d3e9f82328bc96f800f0025f0ae0840ff2f0bf6063e4"
}| Field | Type | Description |
|---|---|---|
isComplete |
bool |
Whether the offer is finalized (accepted, cancelled, or expired) |
createdAt |
uint32 |
Offer creation timestamp |
validUntil |
uint32 |
Expiration timestamp |
acceptedAt |
uint32 |
Timestamp when the offer was accepted (0 if not accepted) |
marketplaceAddress |
address |
Marketplace contract address |
nftAddress |
address |
Target NFT address |
offerOwnerAddress |
address |
Offer creator address |
fullPrice |
coins |
Total offer price |
feesData |
Cell<FeesData> |
Fee configuration (ref cell) |
jettonData |
Cell<JettonData>? |
Jetton configuration (optional ref cell, null for TON offers) |
FeesData:
| Field | Type | Description |
|---|---|---|
marketplaceFeeAddress |
address |
Address that receives marketplace fee |
marketplacePercent |
uint17 |
Marketplace fee, where 100000 = 100% (e.g. 5000 = 5%) |
royaltyAddress |
address? |
Royalty recipient (optional) |
royaltyPercent |
uint17 |
Royalty fee, same format |
JettonData:
| Field | Type | Description |
|---|---|---|
jettonMasterAddress |
address |
Jetton master contract address |
jettonWalletAddress |
address? |
Contract's jetton wallet (set after deploy via DeployJettonOp) |
publicKey |
uint256 |
Marketplace public key for signature verification |
Fees use a denominator of 100000:
marketplacePercent = 5000means 5%royaltyPercent = 2500means 2.5%profitPrice = fullPrice - marketplaceFee - royaltyAmount
Two options:
- With
fullPricein stateInit: setfullPricein initial data and send a message with opcode0x664c0905(DeployTonOp). - With top-up (recommended): set
fullPrice = 0in initial data, deploy withop = 0(comment message) and attach TON. The attached value is added tofullPrice.
- Set
fullPricein stateInit (can be 0 or the target amount). - Set
jettonDatawithjettonMasterAddress,jettonWalletAddress = null, andpublicKey(marketplace Ed25519 public key). - After deployment, send
DeployJettonOpto setjettonWalletAddressandmarketplaceAddress. - If
fullPrice = 0, fund the contract viaJettonTransferNotificationfrom the offer owner.
Important: Do NOT fund the contract with jettons via notification in the same deployment flow. There is no guarantee that the
JettonTransferNotificationwill arrive after the contract is deployed andDeployJettonOpis processed. Use notifications only for top-ups after the contract is fully initialized. To deploy with a pre-set price, setfullPricedirectly in stateInit and transfer jettons to the contract's jetton wallet without notification.
Deploys the contract with TON. No payload processing — the contract simply accepts the message.
Sets jettonWalletAddress and marketplaceAddress with signature verification.
| Field | Type | Description |
|---|---|---|
signature |
bits512 |
Ed25519 signature of the payload cell hash |
payload |
cell |
Ref cell containing jettonWalletAddress + marketplaceAddress |
Access: marketplace or offer owner.
The payload cell is signed by the marketplace backend's private key. This prevents address tampering even if the transaction sender is trusted.
Received when an NFT is transferred to this contract. If sender is the target NFT and the offer is active, triggers acceptOffer:
- TON offer: sends profit, royalty, and marketplace fee as separate TON transfers, then transfers NFT to the offer owner.
- Jetton offer: sends profit, royalty, and marketplace fee as jetton transfers via the contract's jetton wallet, then transfers NFT to the offer owner.
If conditions are not met (wrong NFT, already completed, insufficient msg_value, or offer expired — validUntil <= now), the NFT is returned to the previous owner.
Minimum msg_value: 0.1 TON for TON offers, 0.25 TON for jetton offers.
Top-up the offer with jettons. Validates:
- Contract is a jetton offer and not completed
- Sender is the contract's jetton wallet
- Jetton sender is the offer owner
If validation fails, jettons are returned. Otherwise, fullPrice is increased by the received amount.
Cancel the offer. Body: just the opcode (32 bits).
Access: offer owner or marketplace.
- If sent by marketplace: the attached
msg_valueis returned to the marketplace as a cancel fee. - Jetton offers: jettons are returned to the owner via a single jetton transfer with mode 128 (all balance). Excess TON returns to the owner via
responseDestination. - TON offers: entire contract balance is sent to the owner.
- Text
"cancel": cancels the offer. Access: offer owner only. Marketplace must useCancelOfferOp. - Any other text: top-up the offer with attached TON. Access: offer owner only. Only for TON offers (throws
NotJettonOfferfor jetton offers).
Allows the marketplace to send an arbitrary message from the contract. Only available after the offer is completed (isComplete = true).
| Field | Type | Description |
|---|---|---|
payload |
cell |
Ref cell: uint8 mode + cell message |
Restrictions:
- Sender must be the marketplace.
- Mode
32(destroy) is forbidden. - If
acceptedAt != 0, the message is rejected within ±10 minutes ofacceptedAt.
Anyone can send an external message after validUntil to expire the offer:
- Jetton offers: jettons are returned to the owner.
- TON offers: balance is sent to the owner.
isCompleteis set totrue.
Rejected with 0xffff if the offer has not expired yet.
Returns all contract data as a tuple. Works for both TON and Jetton offers.
| Return value | Type |
|---|---|
isComplete |
bool |
createdAt |
uint32 |
validUntil |
uint32 |
acceptedAt |
uint32 |
marketplaceAddress |
address |
nftAddress |
address |
offerOwnerAddress |
address |
fullPrice |
coins |
marketplaceFeeAddress |
address |
marketplacePercent |
uint17 |
royaltyAddress |
address? |
royaltyPercent |
uint17 |
profitPrice |
int |
jettonMasterAddress |
address? |
jettonWalletAddress |
address? |
Throws ProfitTooLow if the calculated profit is not positive.
| Code | Name | Description |
|---|---|---|
| 1000 | Forbidden |
Sender is not authorized |
| 1001 | OfferCompleted |
Offer is already finalized |
| 1002 | DestroyModeNotAllowed |
Emergency mode 32 is not allowed |
| 1003 | TooCloseToSwap |
Emergency within ±10 min of acceptedAt |
| 1004 | ProfitTooLow |
Calculated profit is not positive |
| 1100 | InvalidSignature |
DeployJettonOp signature verification failed |
| 1101 | NotJettonOffer |
Operation requires a jetton offer |
| 1102 | JettonWalletAlreadySet |
Jetton wallet address is already initialized |
The following checks must be performed off-chain before deploying or interacting with the contract:
- Address workchain: all addresses (
nftAddress,offerOwnerAddress,marketplaceAddress,marketplaceFeeAddress,royaltyAddress) must belong to the correct workchain (typically0). - Jetton wallet address: for jetton offers, compute the correct jetton wallet address for the offer contract by calling
get_wallet_addresson the jetton master. Do not trust user-provided values. - Jetton master verification: verify that
jettonMasterAddressis the actual master of the expected jetton (e.g. USDT). The contract does not verify the jetton type on-chain. - NFT ownership: verify the NFT exists and belongs to the expected owner before creating the offer.
- Fee sanity: ensure
marketplacePercent + royaltyPercent < 100000and thatprofitPrice > 0. validUntil: must be a reasonable future timestamp.fullPrice: Ensure the corresponding jetton or TON amount has actually been transferred to the offer's smart contract.- Signature payload: the
DeployJettonOppayload must be signed by the private key corresponding topublicKeyinJettonData. Sign the hash of the cell containingjettonWalletAddress+marketplaceAddress.
Универсальный контракт оффера на NFT, написанный на Tolk для блокчейна TON. Поддерживает офферы как в TON, так и в жетонах (например, USDT).
contracts/— исходный код смарт-контракта на Tolkwrappers/— TypeScript-обёртки для взаимодействия с контрактомtests/— Jest-тесты с использованием@ton/sandboxscripts/— скрипты деплоя
npx blueprint build
npx blueprint test| Поле | Тип | Описание |
|---|---|---|
isComplete |
bool |
Завершён ли оффер (принят, отменён или истёк) |
createdAt |
uint32 |
Временная метка создания |
validUntil |
uint32 |
Временная метка истечения |
acceptedAt |
uint32 |
Временная метка принятия (0, если не принят) |
marketplaceAddress |
address |
Адрес контракта маркетплейса |
nftAddress |
address |
Адрес NFT |
offerOwnerAddress |
address |
Адрес создателя оффера |
fullPrice |
coins |
Полная цена оффера |
feesData |
Cell<FeesData> |
Конфигурация комиссий (ref cell) |
jettonData |
Cell<JettonData>? |
Конфигурация жетона (опционально, null для TON-офферов) |
FeesData:
| Поле | Тип | Описание |
|---|---|---|
marketplaceFeeAddress |
address |
Адрес получателя комиссии маркетплейса |
marketplacePercent |
uint17 |
Комиссия маркетплейса, 100000 = 100% (например, 5000 = 5%) |
royaltyAddress |
address? |
Получатель роялти (опционально) |
royaltyPercent |
uint17 |
Роялти, тот же формат |
JettonData:
| Поле | Тип | Описание |
|---|---|---|
jettonMasterAddress |
address |
Адрес мастер-контракта жетона |
jettonWalletAddress |
address? |
Жетон-кошелёк контракта (устанавливается после деплоя через DeployJettonOp) |
publicKey |
uint256 |
Публичный ключ маркетплейса для проверки подписи |
Знаменатель: 100000:
marketplacePercent = 5000означает 5%royaltyPercent = 2500означает 2.5%profitPrice = fullPrice - marketplaceFee - royaltyAmount
Два варианта:
- С
fullPriceв stateInit: указатьfullPriceв начальных данных и отправить сообщение с опкодом0x664c0905(DeployTonOp). - С пополнением (рекомендуется): указать
fullPrice = 0, задеплоить сop = 0(комментарий) и приложить TON. Приложенная сумма добавляется кfullPrice.
- Указать
fullPriceв stateInit (может быть 0 или целевая сумма). - Указать
jettonDataсjettonMasterAddress,jettonWalletAddress = nullиpublicKey(Ed25519 публичный ключ маркетплейса). - После деплоя отправить
DeployJettonOpдля установкиjettonWalletAddressиmarketplaceAddress. - Если
fullPrice = 0, пополнить контракт черезJettonTransferNotificationот владельца оффера.
Важно: НЕ отправляйте жетоны на контракт через нотификацию в рамках деплоя. Нет гарантии, что
JettonTransferNotificationпридёт после того, как контракт будет задеплоен и обработанDeployJettonOp. Используйте нотификации только для пополнения уже полностью инициализированного контракта. Чтобы задеплоить с заданной ценой, укажитеfullPriceпрямо в stateInit и переведите жетоны на жетон-кошелёк контракта без нотификации.
Деплоит контракт с TON. Payload не обрабатывается — контракт просто принимает сообщение.
Устанавливает jettonWalletAddress и marketplaceAddress с проверкой подписи.
| Поле | Тип | Описание |
|---|---|---|
signature |
bits512 |
Ed25519 подпись хеша payload cell |
payload |
cell |
Ref cell: jettonWalletAddress + marketplaceAddress |
Доступ: маркетплейс или владелец оффера.
Payload подписывается приватным ключом бэкенда маркетплейса. Это предотвращает подмену адресов даже если отправитель транзакции доверенный.
Приходит, когда NFT переводится на этот контракт. Если отправитель — целевая NFT и оффер активен, запускается acceptOffer:
- TON-оффер: отправляет профит, роялти и комиссию маркетплейса отдельными TON-переводами, затем переводит NFT владельцу оффера.
- Жетон-оффер: отправляет профит, роялти и комиссию маркетплейса жетон-переводами через жетон-кошелёк контракта, затем переводит NFT.
Если условия не выполнены (не та NFT, уже завершён, недостаточный msg_value, или оффер истёк — validUntil <= now), NFT возвращается предыдущему владельцу.
Минимальный msg_value: 0.1 TON для TON-офферов, 0.25 TON для жетон-офферов.
Пополнение оффера жетонами. Проверяет:
- Контракт — жетон-оффер и не завершён
- Отправитель — жетон-кошелёк контракта
- Отправитель жетонов — владелец оффера
При ошибке валидации жетоны возвращаются. Иначе fullPrice увеличивается на полученную сумму.
Отмена оффера. Body: только опкод (32 бита).
Доступ: владелец оффера или маркетплейс.
- Если от маркетплейса: приложенный
msg_valueвозвращается маркетплейсу как комиссия за отмену. - Жетон-офферы: жетоны возвращаются владельцу одной транзакцией с mode 128 (весь баланс). Излишки TON возвращаются через
responseDestination. - TON-офферы: весь баланс контракта отправляется владельцу.
- Текст
"cancel": отменяет оффер. Доступ: только владелец оффера. Маркетплейс должен использоватьCancelOfferOp. - Любой другой текст: пополнение оффера приложенными TON. Доступ: только владелец оффера. Только для TON-офферов (бросает
NotJettonOfferдля жетон-офферов).
Позволяет маркетплейсу отправить произвольное сообщение от имени контракта. Доступно только после завершения оффера (isComplete = true).
| Поле | Тип | Описание |
|---|---|---|
payload |
cell |
Ref cell: uint8 mode + cell message |
Ограничения:
- Отправитель — только маркетплейс.
- Mode
32(уничтожение) запрещён. - Если
acceptedAt != 0, сообщение отклоняется в пределах ±10 минут отacceptedAt.
Любой может отправить внешнее сообщение после validUntil для истечения оффера:
- Жетон-офферы: жетоны возвращаются владельцу.
- TON-офферы: баланс отправляется владельцу.
isCompleteустанавливается вtrue.
Отклоняется с 0xffff, если оффер ещё не истёк.
Возвращает все данные контракта как tuple. Работает и для TON, и для жетон-офферов.
| Возвращаемое значение | Тип |
|---|---|
isComplete |
bool |
createdAt |
uint32 |
validUntil |
uint32 |
acceptedAt |
uint32 |
marketplaceAddress |
address |
nftAddress |
address |
offerOwnerAddress |
address |
fullPrice |
coins |
marketplaceFeeAddress |
address |
marketplacePercent |
uint17 |
royaltyAddress |
address? |
royaltyPercent |
uint17 |
profitPrice |
int |
jettonMasterAddress |
address? |
jettonWalletAddress |
address? |
Бросает ProfitTooLow, если вычисленный профит не положителен.
| Код | Имя | Описание |
|---|---|---|
| 1000 | Forbidden |
Отправитель не авторизован |
| 1001 | OfferCompleted |
Оффер уже завершён |
| 1002 | DestroyModeNotAllowed |
Emergency mode 32 запрещён |
| 1003 | TooCloseToSwap |
Emergency в пределах ±10 мин от acceptedAt |
| 1004 | ProfitTooLow |
Вычисленный профит не положителен |
| 1100 | InvalidSignature |
Проверка подписи DeployJettonOp не пройдена |
| 1101 | NotJettonOffer |
Операция требует жетон-оффер |
| 1102 | JettonWalletAlreadySet |
Адрес жетон-кошелька уже установлен |
Следующие проверки обязательны перед деплоем или взаимодействием с контрактом:
- Воркчейн адресов: все адреса (
nftAddress,offerOwnerAddress,marketplaceAddress,marketplaceFeeAddress,royaltyAddress) должны принадлежать правильному воркчейну (обычно0). - Адрес жетон-кошелька: для жетон-офферов вычислить правильный адрес жетон-кошелька контракта, вызвав
get_wallet_addressна жетон-мастере. Не доверять значениям от пользователя. - Верификация жетон-мастера: убедиться, что
jettonMasterAddress— это мастер нужного жетона (например, USDT). Контракт не проверяет тип жетона on-chain. - Владение NFT: убедиться, что NFT существует и принадлежит ожидаемому владельцу.
- Корректность комиссий:
marketplacePercent + royaltyPercent < 100000иprofitPrice > 0. validUntil: должен быть разумной будущей временной меткой.fullPrice: Убедиться, что соответствующее количество жетонов или TON действительно переведено на контракта оффера.- Подпись payload: payload
DeployJettonOpдолжен быть подписан приватным ключом, соответствующимpublicKeyвJettonData. Подписывается хеш cell, содержащейjettonWalletAddress+marketplaceAddress.